引言

在开发过程中,通常需要创建其名称存储在XML配置文件中的类的实例,或者调用其名称以字符串形式写入的方法作为注释属性的值。 在这种情况下,答案是一个:“使用反射!”。
在新版本的CUBA Platform中,改善框架的任务之一是摆脱在UI屏幕的控制器类中显式创建事件处理程序的过程。 在以前的版本中,控制器初始化方法中的处理程序声明与代码非常混乱,因此在第七个版本中,我们决定清除所有内容。
事件侦听器只是对在正确的时间需要调用的方法的引用(请参阅Observer模板 )。 使用java.lang.reflect.Method
类可以很容易地实现这种模板。 开始时,您只需要扫描类,从它们中提取带注释的方法,保存对它们的引用,并在事件发生时使用链接调用一个(或多个)方法,就像在大多数框架中一样。 唯一令我们沮丧的是,传统上,UI中会生成许多事件,并且在使用反射API时,您必须以方法调用时间的形式付出一定的代价。 因此,我们决定研究如何在不使用反射的情况下制作事件处理程序。
我们已经在habr上发布了有关MethodHandles和LambdaMetafactory的材料 ,并且该材料是一种延续。 我们将研究使用反射API的优缺点以及替代方法-使用AOT编译和LambdaMetafactory生成代码,以及在CUBA框架中如何使用它。
反思:老。 好啊 可靠的
在计算机科学中,反射或反射(内省的总称,英语反射)表示程序可以在运行时跟踪和修改其自身的结构和行为的过程。 (c)维基百科。
对于大多数Java开发人员而言,反思从来都不是新鲜事物。 在我看来,没有这种机制,Java就不会成为Java,它现在在应用程序软件开发中占据着很大的市场份额。 试想一下:在第一个JDK版本中,通过注释,依赖项注入,方面来代理,将方法绑定到事件,甚至实例化JDBC驱动程序! 到处反射,是所有现代框架的基石。
在执行我们的任务时,Reflection是否有任何问题? 我们确定了三个:
速度 -通过Reflection API的方法调用比直接调用慢。 在JVM的每个新版本中,开发人员都通过反射不断提高调用速度,JIT编译器试图进一步优化代码,但是无论如何,与直接方法调用相比,差异是显而易见的。
键入 -如果在代码中使用java.lang.reflect.Method
,则这只是对某些方法的引用。 而且没有写任何地方传递多少参数和它们是什么类型。 参数错误的调用将在运行时生成错误,而不是在编译或下载应用程序的阶段。
透明度 -如果通过反射调用的方法失败,那么在深入探究错误的真正原因之前,我们将不得不经过几次invoke()
调用。
但是,如果我们查看Hibernate中的Spring或JPA事件处理程序的代码,那么好的旧java.lang.reflect.Method
将在其中。 而且在不久的将来,我认为这种情况不太可能改变。 这些框架太大,与它们的联系太多,似乎服务器端事件处理程序的性能足以考虑可以通过反射替换调用的内容。
还有什么其他选择?
AOT编译和代码生成-加快应用程序的速度!
替换反射API的第一个候选对象是代码生成。 现在,诸如Micronaut或Quarkus之类的框架已经开始出现,它们试图解决两个问题:降低应用程序的启动速度和减少内存消耗。 在我们的容器,微服务和无服务器架构时代,这两个指标至关重要。新的框架正在尝试通过AOT编译解决这一问题。 使用不同的技术(例如,您可以在此处阅读),以某种方式修改应用程序代码,以便对方法,构造函数等进行所有自反调用。 改为直接致电。 因此,您不需要在应用程序启动时扫描类并创建bean,并且JIT在运行时更有效地优化了代码,这大大提高了基于此类框架构建的应用程序的性能。 这种方法是否有缺点? 答:当然有。
首先,您不运行编写的代码,因为源代码在编译过程中会发生变化,因此,如果出现问题,有时很难理解错误的出处:在代码中还是在生成算法中(通常在您的代码中) ) 从这里开始出现调试问题-您必须调试自己的代码。
第二个-要运行用AOT编译框架编写的应用程序,您需要一个特殊的工具。 例如,您不仅可以获取并运行以Quarkus编写的应用程序。 我们需要一个用于maven / gradle的特殊插件,它将对您的代码进行预处理。 现在,如果框架中出现错误,则不仅需要更新库,还需要更新插件。
实际上,代码生成在Java世界中也不是新鲜事物;它在Micronaut或Quarkus中并未出现。 在某些形式中,某些框架会使用它。 在这里,我们可以回想起lombok,aspectj,它具有用于Aspects或eclipselink的代码的初步生成,它将代码添加到实体类中以实现更有效的反序列化。 在CUBA,我们使用代码生成来生成有关实体状态变化的事件,并在类代码中包含验证器消息,以简化在UI中使用实体的工作。
对于CUBA开发人员而言,为事件处理程序实现静态代码生成将是一个极端的步骤,因为必须在内部体系结构和插件中进行很多更改才能生成代码。 有什么看起来像反射但速度更快的东西吗?
Java 7为JVM引入了一条新指令invokedynamic
。 关于她,弗拉基米尔·伊万诺夫(Vladimir Ivanov)在这里的 jug.ru上发表了出色的报道。 该指令最初被设想用于动态语言(例如Groovy),是在不使用反射的情况下调用Java方法的理想选择。 与新指令同时,相关的API出现在JDK中:
MethodHandle
类-早在Java 7中就出现了,但仍然不是很常用LambdaMetafactory
该类已经来自Java 8,它成为用于动态调用的API的进一步开发,并在其中使用MethodHandle
。
似乎MethodHandle
本质上是方法(构造函数等)的类型化指针,将能够实现java.lang.reflect.Method
的角色。 并且调用将更快,因为在MethodHandle
时,在反射API中对每个调用执行的所有类型检查在这种情况下仅执行一次。
但是,事实证明,纯MethodHandle
速度甚至比通过反射API的调用还要慢。 通过将MethodHandle
静态可以实现性能提升,但并非在所有情况下都可以实现。 关于OpenJDK邮件列表中MethodHandle
调用速度的讨论非常精彩。
但是,当LambdaMetafactory
类LambdaMetafactory
,确实有机会加快方法调用的速度。 LambdaMetafactory
允许LambdaMetafactory
创建一个lambda对象并在其中包装直接方法调用,可以通过MethodHandle
获得该MethodHandle
。 然后,使用生成的对象,可以调用所需的方法。 这是生成的示例,其中包装了作为参数传递给BiFunction的getter方法:
private BiFunction createGetHandlerLambda(Object bean, Method method) throws Throwable { MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "apply", MethodType.methodType(BiFunction.class), MethodType.methodType(Object.class, Object.class, Object.class), caller.findVirtual(bean.getClass(), method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()[0])), MethodType.methodType(method.getReturnType(), bean.getClass(), method.getParameterTypes()[0])); MethodHandle factory = site.getTarget(); BiFunction listenerMethod = (BiFunction) factory.invoke(); return listenerMethod; }
结果,我们得到了BiFunction的实例而不是Method的实例。 现在,即使我们在代码中使用了Method,将其替换为BiFunction也并不困难。 采取真实的(略微简化,真实的)代码来调用方法处理程序,在Spring Framework @EventListener
其标记为@EventListener
:
public class ApplicationListenerMethodAdapter implements GenericApplicationListener { private final Method method; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = this.method.invoke(bean, event); handleResult(result); } }
这是相同的代码,但是使用通过lambda的方法调用:
public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter { private final BiFunction funHandler; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = funHandler.apply(bean, event); handleResult(result); } }
更改最少,功能相同,但有优点:
Lambda具有类型 -它是在创建时指定的,因此调用“仅方法”将失败。
跟踪堆栈更短 -通过lambda调用方法时,仅添加了一个附加调用apply()
。 仅此而已。 接下来,调用方法本身。
但是必须测量速度。
测量速度
为了检验假设,我们使用JMH进行了微基准测试,以比较通过不同方式调用同一方法的执行时间和吞吐量:通过反射API,通过LambdaMetafactory,还添加了直接方法调用以进行比较。 在测试开始之前,已创建并缓存到Method和lambda的链接。
测试参数:
@BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
如果有兴趣,可以从GitHub下载测试本身,并自己运行。
Oracle JDK 11.0.2和JMH 1.21的测试结果(数字可能有所不同,但是差异仍然明显且大致相同):
测试-获得价值 | 吞吐量(运营/我们) | 执行时间(我们/运营) |
---|
LambdaGetTest | 72 | 0.0118 |
ReflectionGetTest | 65岁 | 0.0177 |
DirectMethodGetTest | 260 | 0.0048 |
测试-设定值 | 吞吐量(运营/我们) | 执行时间(我们/运营 |
LambdaSetTest | 96 | 0.0092 |
ReflectionSetTest | 58 | 0.0173 |
DirectMethodSetTest | 415 | 0.0031 |
平均而言,事实证明,通过lambda调用方法比通过反射API快约30%。 如果有人对细节感兴趣的话, 这里还有一个关于方法调用性能的精彩讨论。 简而言之-速度的提高也归因于这样一个事实,即生成的lambda可以内联到程序代码中,并且与反射不同,尚未执行类型检查。
当然,该基准测试非常简单,它不包括在类层次结构中调用方法或不测量调用最终方法的速度。 但是我们进行了更复杂的测量,结果始终支持使用LambdaMetafactory。
使用方法
在CUBA版本7框架中,在UI控制器中,可以使用@Subscribe
批注对某些用户界面事件“签名”一种方法。 在内部,这是在LambdaMetafactory
上LambdaMetafactory
,指向监听器方法的链接是在第一次调用时创建并缓存的。
这项创新使得可以极大地清除代码,尤其是在具有大量元素,复杂交互以及相应地具有大量事件处理程序的表单中。 CUBA快速入门中的一个简单示例:想象一下,添加或删除产品项目时需要重新计算订单金额。 当实体中的集合更改时,您需要编写代码来运行calculateAmount()
方法。 之前的样子:
public class OrderEdit extends AbstractEditor<Order> { @Inject private CollectionDatasource<OrderLine, UUID> linesDs; @Override public void init( Map<String, Object> params) { linesDs.addCollectionChangeListener(e -> calculateAmount()); } ... }
在CUBA 7中,代码如下所示:
public class OrderEdit extends StandardEditor<Order> { @Subscribe(id = "linesDc", target = Target.DATA_CONTAINER) protected void onOrderLinesDcCollectionChange (CollectionChangeEvent<OrderLine> event) { calculateAmount(); } ... }
底线:代码更简洁,并且没有神奇的init()
方法,该方法倾向于增长并填充事件处理程序,并且表单的复杂性也随之增加。 但是-我们甚至不需要在要订购的组件上填写字段,CUBA就会通过ID找到该组件。
结论
尽管出现了带有AOT编译的新一代框架( Micronaut , Quarkus ),它们相对于“传统”框架(主要是与Spring相比)具有不可否认的优势,但是仍然存在大量使用反射API编写的代码(并感谢所有相同的Spring)。 而且看起来Spring框架目前仍是应用程序开发框架中的领导者,并且我们将在很长一段时间内使用基于反射的代码。
如果您正在考虑在代码中使用Reflection API(无论是应用程序还是框架),请三思。 首先,关于代码生成,然后关于MethodHandles / LambdaMetafactory。 第二种方法可能会更快,并且开发工作的花费不会超过使用Reflection API的花费。
一些更有用的链接:
Java反射的更快替代品
在Java中破解Lambda表达式
Java中的方法处理
Java反射,但速度更快
为什么LambdaMetafactory比静态MethodHandle慢10%但比非静态MethodHandle快80%?
太快了,太变态了:什么会影响Java中的方法调用性能?