📜 ⬆️ ⬇️

Do not make listeners to reflect

Introduction



In the development process, it is often necessary to create an instance of a class whose name is stored in the XML configuration file, or to call a method whose name is written as a string as the value of the annotation attribute. In such cases, the answer is one: “Use reflection!”.


In the new version of the CUBA Platform, one of the tasks to improve the framework was to get rid of the explicit creation of event handlers in the controller classes of UI screens. In previous versions, the declarations of handlers in the controller's initialization method were very cluttering up the code, so in the seventh version we decided to clean everything from there.


An event listener is just a reference to the method that you need to call at the right time (see the Observer template ). Such a template is quite simple to implement using the java.lang.reflect.Method class. At the start, you just need to scan the classes, pull out annotated methods from them, save references to them, and use references to call the method (or methods) when an event occurs, as is done in the bulk of frameworks. The only thing that stopped us is that a lot of events are traditionally generated in the UI, and when using the reflection API you have to pay some price in the form of method call time. Therefore, we decided to look at how you can still make event handlers without using reflection.


We have already published on Habré materials about MethodHandles and LambdaMetafactory , and this material is a kind of continuation. We will consider the pros and cons of using the reflection API, as well as alternatives - code generation with AOT compilation and LambdaMetafactory, and how it was applied in the CUBA framework.


Reflection: Old. Kind. Reliable


In computer science, reflection or reflection (holonim of introspection, English reflection) means a process during which a program can monitor and modify its own structure and behavior at run time. (c) Wikipedia.


For most Java developers, reflection is never a new thing. It seems to me that without this mechanism, Java would not have become the Java, which now occupies a large market share in the development of application software. Just think: proxying, binding methods to events through annotations, dependency injection, aspects, and even instantiating the JDBC driver in the very first versions of the JDK! Reflection everywhere is the cornerstone of all modern frameworks.


Are there any problems with Reflection in relation to our task? We identified three:


Speed - calling a method via the Reflection API is slower than a direct call. In each new version of JVM, developers all the time speed up calls through reflection, the JIT compiler tries to optimize the code even more, but the difference is still noticeable compared to a direct method call.


Typing - if you use java.lang.reflect.Method in code, then this is just a link to some method. And nowhere is it written how many parameters are transmitted and what type they are. A call with incorrect parameters will generate an error at runtime, and not at the stage of compiling or loading the application.


Transparency - if the method called through reflection falls down with an error, then we will have to wade through several invoke() calls before we get to the real cause of the error.


But if we look at the code of the event handlers Spring or JPA callbacks in Hibernate, then there will be good old java.lang.reflect.Method inside. And in the near future, I think this is unlikely to change. These frameworks are too large and too much is tied to them, and it seems that the server-side event handlers have enough performance to think about what to replace calls through reflection.


What other options are there?


AOT compilation and code generation - return speed to applications!


The first candidate to replace the reflection API is code generation. Now frameworks such as Micronaut or Quarkus have begun to appear, which are trying to solve two problems: reducing the launch speed of the application and reducing memory consumption. These two metrics are vital in our age of containers, microservices and serverless architectures, and new frameworks are trying to solve this by AOT compilation. Using different techniques (you can read here , for example), the application code is modified in such a way that all reflexive calls to methods, constructors, etc. replaced by direct calls. Thus, you do not need to scan classes and create bins at the time of launching the application, and JIT more effectively optimizes the code at runtime, which gives a significant increase in the performance of applications built on such frameworks. Does this approach have disadvantages? The answer: of course, is.


First, you run the wrong code you wrote. The source code changes during compilation, so if something goes wrong, it is sometimes difficult to understand where the error is: in your code or in the generation algorithm (usually, in your code, of course ). And the debugging problem follows from here - debugging does not have to be your code.


The second is that you need a special tool to run an application written on the framework with AOT compilation. You can not just take and run an application written in Quarkus, for example. You need a special plugin for maven / gradle, which will pre-process your code. And now, in case of detection of errors in the framework, you need to update not only the library, but also the plugin.


Truth be told, code generation is also not news in the Java world, it did not appear with Micronaut or Quarkus . In one form or another, some frameworks use it. Here you can recall lombok, aspectj with its preliminary code generation for aspects, or eclipselink, which adds code to the entity classes for more efficient deserialization. In CUBA, we use code generation to generate entity state change events and to include validator messages in class code to simplify work with entities in the UI.


For CUBA developers, implementing static code generation for event handlers would be a somewhat extreme step, because a lot of changes had to be made in the internal architecture and in the plugin to generate code. Is there something that looks like reflection, but faster?


LambdaMetafactory - the same method calls, but faster


In Java 7, a new instruction for JVM - invokedynamic . About her there is an excellent report by Vladimir Ivanov on jug.ru here . Originally conceived for use in dynamic languages ​​like Groovy, this instruction has become an excellent candidate to invoke methods in Java without using reflection. Simultaneously with the new instruction, the associated API appeared in the JDK:



It seemed that MethodHandle , in essence being a typed pointer to a method (constructor, etc.), would be able to perform the role of java.lang.reflect.Method . And the calls will be faster, because all type-matching checks that are executed in the Reflection API on each call, in this case, are performed only once, when creating MethodHandle .


But alas, the pure MethodHandle was even slower than calls through the reflection API. You can achieve better performance if you make the MethodHandle static, but not in all cases it is possible. There is a great discussion about the speed of MethodHandle calls on the OpenJDK mailing list .


But when the LambdaMetafactory class LambdaMetafactory , there was a real chance to speed up method calls. LambdaMetafactory allows LambdaMetafactory to create a lambda object and wrap a direct method call in it, which can be obtained through MethodHandle . And then, using the generated object, you can call the desired method. Here is an example of generation that “wraps” the getter method passed as a parameter in BiFunction:


 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; } 

As a result, we get a copy of BiFunction instead of Method. And now, even if we used Method in our code, then replacing it with BiFunction is not difficult. Take the real (slightly simplified, true) code calling the method handler marked with the Spring Framework @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); } } 

And here is the same code, but which uses the method call via 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); } } 

Minimal changes, functionality is the same, but there are advantages:


Lambda has a type - it is specified when creating it, so calling “just a method” will not work.


The stack of the trace is shorter - when calling a method via lambda, only one additional call is added — apply() . And that's all. The method itself is then called.


But the speed must be measured.


Measure the speed


To test the hypothesis, we made a micro-benchmark using JMH to compare execution time and throughput when calling the same method in different ways: through the reflection API, through the LambdaMetafactory, and also added a direct method call for comparison. References to Method and lambda were created and cached before running the test.


Test parameters:


 @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) 

The test itself can be downloaded from GitHub and run it yourself if interested.


Test results for Oracle JDK 11.0.2 and JMH 1.21 (numbers may differ, but the difference remains noticeable and about the same):


Test - Get ValueThroughput (ops / us)Execution Time (us / op)
LambdaGetTest720.0118
ReflectionGetTest650.0177
DirectMethodGetTest2600.0048
Test - Set ValueThroughput (ops / us)Execution Time (us / op
Lambdasettest960.0092
ReflectionSetTest580.0173
DirectMethodSetTest4150.0031

On average, it turned out that the method call via lambda is about 30% faster than through the reflection API. There is another great discussion about the performance of calling methods here , if someone is interested in details. In short, the speed gain is also due to the fact that the generated lambdas can be inlined in the program code, and type checks are not performed yet, unlike reflection.


Of course, this benchmark is pretty simple; it does not include calling methods over a class hierarchy or measuring the speed of calling final methods. But we did more complex measurements, and the results were always in favor of using the LambdaMetafactory.


Using


In the CUBA version 7 framework, in the UI controllers, you can use the @Subscribe annotation to “sign” a method for certain user interface events. Inside it is implemented on the LambdaMetafactory , references to methods-listeners are created and cached upon the first call.


This innovation made it possible to greatly clean the code, especially in the case of forms with a large number of elements, complex interactions and, accordingly, with a large number of event handlers. A simple example from CUBA QuickStart: Imagine that you need to recalculate the order amount when adding or deleting items of goods. You need to write code that runs the calculateAmount() method when the collection changes in essence. How it looked before:


 public class OrderEdit extends AbstractEditor<Order> { @Inject private CollectionDatasource<OrderLine, UUID> linesDs; @Override public void init( Map<String, Object> params) { linesDs.addCollectionChangeListener(e -> calculateAmount()); } ... } 

And in CUBA 7, the code looks like this:


 public class OrderEdit extends StandardEditor<Order> { @Subscribe(id = "linesDc", target = Target.DATA_CONTAINER) protected void onOrderLinesDcCollectionChange (CollectionChangeEvent<OrderLine> event) { calculateAmount(); } ... } 

The bottom line: the code is cleaner and there is no magic init() method that tends to grow and fill with event handlers as the complexity of the form increases. And also - we don’t even need to make a field with a component to which we subscribe, CUBA will find this component by ID.


findings


Despite the emergence of a new generation of frameworks with AOT compilation ( Micronaut , Quarkus ), which have indisputable advantages over “traditional” frameworks (basically, they are compared to Spring ), there is still a huge amount of code in the world, which is written using the reflection API (and for this thanks to all the same Spring). And it seems that the Spring Framework is currently still the leader among the frameworks for application development and we will work with reflection-based code for a long time.


And if you are thinking about using the Reflection API in your code - whether it is an application or a framework - think twice. First, about code generation, and then about MethodHandles / LambdaMetafactory. The second method may be faster, and the development effort will be spent no more than when using the Reflection API.


Some more useful links:
A faster alternative to Java Reflection
Hacking Lambda Expressions in Java
Method Handles in Java
Java Reflection, but much faster
Why is LambdaMetafactory 10% slower than a static MethodHandle but 80% faster than a non-static MethodHandle?
Too Fast, Too Megamorphic: what influences method call performance in Java?


')

Source: https://habr.com/ru/post/447022/


All Articles