All Articles ↓
6 months ago
Think Twice Before Using Reflection

Introduction

Sometimes, as a developer, you may bump into a situation when it’s not possible to instantiate an object using the new operator because its class name is stored somewhere in configuration XML or you need to invoke a method which name is specified as an annotation property. In such cases, you always have an answer: “Use reflection!”.

In the new version of CUBA framework, we decided to improve many aspects of the architecture and one of the most significant change was deprecating “classic“ event listeners in the controllers UI. In the previous version of the framework, a lot of boilerplate code registering listeners in screen's init() method made your code almost unreadable, so the new concept should have cleaned this up.

You can always implement method listener by storing java.lang.reflect.Method instances for annotated methods and invoke them like it is implemented in many frameworks, but we decided to have a look at other options. Reflection calls have their cost and if you develop a production-class framework, even tiny improvement may pay back in a short time.

In this article, we’ll look at reflection API, pros and cons for its usage and review other options to replace reflection API calls - AOT and code generation and LambdaMetafactory.

Reflection - good old reliable API

"Reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime" according to Wikipedia.

For most Java developers reflection is not a new thing and it is used in many cases. I’d dare to say that Java won’t become what it is now without reflection. Just think about annotation processing, data serialization, method binding via annotations or configuration files… For the most popular IoC frameworks reflection API is a cornerstone because of extensive usage of class proxying, method reference usage, etc. Also, you can add aspect-oriented programming to this list - some AOP frameworks rely on reflection for method execution interception.

Are there any problems with reflection? We can think about three of them:

Speed - reflection calls are slower than direct calls. We can see a great improvement in reflection API performance with every JVM release, JIT compiler’s optimization algorithms are getting better, but reflective method invocations are still about three times slower than direct ones.

Type safety - if you use method reference in your code, it is just a method reference. If you write a code that invokes a method via its reference and passes wrong parameters, the invocation will fail at runtime, not at compile time or load-time.

Traceability - if a reflective method call fails, it might be tricky to find a line of code that caused this, because stack trace is usually huge. You need to dig really deep into all these invoke() and proxy() calls.

But if you look into event listener implementations in Spring or JPA callbacks in Hibernate - you will see familiar java.lang.reflect.Method references inside. And I doubt that it will be changed in the nearest future - mature frameworks are big and complex, used in many mission-critical systems, so developers should introduce big changes carefully.

Let’s have a look at other options.

AOT compilation and code generation - make applications fast again

The first candidate for reflection replacement - code generation. Nowadays we can see a rise of new frameworks like Micronaut and Quarkus that are targeted to two aims: fast start time and low memory footprint. Those two metrics are vital in the age of microservices and serverless applications. And recent frameworks are trying to get rid of reflection completely by using ahead-of-time compilation and code generation. By using annotation processing, type visitors and other techniques they add direct method calls, object instantiations, etc. into your code, therefore making applications faster. Those do not create and inject beans during startup using Class.newInstance(), do not use reflective method calls in listeners, etc. Looks very promising, but are there any trade-offs here? And the answer is - yes.

The first one - you run the code that is not yours exactly. Code generation changes your original code, therefore if something goes wrong you cannot tell whether it is your mistake or it is a glitch in the code processing algorithms. And don’t forget that now you should debug generated code, but not your code.

The second trade-off - you must use a separate tool/plugin provided by the vendor to use the framework. You cannot “just” run the code, you should pre-process it in a special way. And if you use the framework in production, you should apply the vendor’s bugfixes to both framework codebase and code processing tool.

Code generation has been known for a long time, it hasn’t appeared with Micronaut or Quarkus. For example, in CUBA we use class enhancement during compile-time using custom Grails plugin and Javassist library. We add extra code to generate entity update events and include bean validation messages to the class code as String fields for the nice UI representation.

But implementing code generation for event listeners looked a bit extreme because it would require a complete change of the internal architecture. Is there such a thing as reflection, but faster?

LambdaMetafactory - faster method invocation

In Java 7, there was introduced a new JVM instruction - invokedynamic. Initially targeted at dynamic languages implementations based on JVM, it has become a good replacement for API calls. This API may give us a performance improvement over traditional reflection. And there are special classes to construct invokedynamic calls in your Java code:

  • MethodHandle - this class was introduced in Java 7, but it is still not well-known.

  • LambdaMetafactory - was introduced in Java 8. It is further development of dynamic invocation idea. This API is based on MethodHandle.

Method handles API is a good replacement for standard reflection because JVM will perform all pre-invocation checks only once - during MethodHandle creation. Long story short - a method handle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation, with optional transformations of arguments or return values.

Surprisingly, pure MethodHandle reference invocation does not provide better performance comparing to reflection API unless you make MethodHandle references static as discussed in this email list.

But LambdaMetafactory is another story - it allows us to generate an instance of a functional interface in the runtime that contains a reference to a method resolved by MethodHandle. Using this lambda object, we can invoke the referenced method directly. Here is an example:



private BiConsumer createVoidHandlerLambda(Object bean, Method method) throws Throwable {
        MethodHandles.Lookup caller = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(caller,
                "accept",
                MethodType.methodType(BiConsumer.class),
                MethodType.methodType(void.class, Object.class, Object.class),
                caller.findVirtual(bean.getClass(), method.getName(),
                        MethodType.methodType(void.class, method.getParameterTypes()[0])),
                MethodType.methodType(void.class, bean.getClass(), method.getParameterTypes()[0]));
        MethodHandle factory = site.getTarget();
        BiConsumer listenerMethod = (BiConsumer) factory.invoke();
        return listenerMethod;
    }
 

Please note that with this approach we can just use java.util.function.BiConsumer instead of java.lang.reflect.Method, therefore it won’t require too much refactoring. Let's consider event listener handler code - it is a simplified adaptation from Spring Framework:


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 that is how it can be changed with Lambda-based method reference:

public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter {
    private final BiFunction funHandler;
    public void onApplicationEvent(ApplicationEvent event) {
        Object bean = getTargetBean();
        Object result = handler.apply(bean, event);
        handleResult(result);
    }
}

The code has subtle changes and functionality is the same. But it has some advantages over traditional reflection:

Type safety - you specify method signature in LambdaMetafactory.metafactory call, therefore you won’t be able to bind “just” methods as event listeners.

Traceability - lambda wrapper adds only one extra call to method invocation stack trace. It makes debugging much easier.

Speed - this is a thing that should be measured.

Benchmarking

For the new version of CUBA framework, we created a JMH-based microbenchmark to compare execution time and throughput for “traditional” reflection method call, lambda-based one and we added direct method calls just for comparison. Both method references and lambdas were created and cached before test execution.

We used the following benchmark testing parameters:

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

You can download the benchmark from GitHub and run the test by yourself.

For JVM 11.0.2 and JMH 1.21 we got the following results (numbers may slightly vary from run to run):

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

As you can see, the lambda-based method handlers are about 30% faster on the average. There is a good discussion here regarding lambda-based method invocation performance. The outcome - classes generated by LambdaMetafactory can be inlined, gaining some performance improvement. And it is faster than reflection because reflective calls had to pass security checks on every invocation.

This benchmark is pretty anemic and does not take into account class hierarchy, final methods, etc., it measures “just” method calls, but it was sufficient for our purpose.

Implementation

In CUBA you can use @Subscribe annotation to make a method “listen” to various CUBA-specific application events. Internally we use this new MethodHandles/LambdaMetafactory based API for faster listener invocations. All the method handles are cached after the first invocation.

The new architecture has made the code cleaner and more manageable, especially in case of complex UI with a lot of event handlers. Just have a look at the simple example. Assume that you need to recalculate order amount based on products added to this order. You have a method calculateAmount() and you need to invoke it as soon as a collection of products in the order has changed. Here is the old version of the UI controller:

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 here how it looks in the new version:

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

The code is cleaner and we were able to get rid of the “magic” init() method that is usually stuffed with event handler creation statements. And we don’t even need to inject data component into the controller - the framework will find it by the component ID.

Conclusion

Despite the recent introduction of the new generation of the frameworks (Micronaut, Quarkus) that have some advantages over “traditional” frameworks, there is a huge amount of reflection-based code, thanks to Spring. We’ll see how the market will change in the nearest future, but nowadays Spring is the obvious leader among Java application frameworks, therefore we’ll be dealing with the reflection API for quite a long time.

And if you think about using reflection API in your code, whether you’re implementing your own framework or just an application, consider two other options - code generation and, especially, LambdaMetafactory. The latter will increase code execution speed, whilst development won’t take more time compared to “traditional” reflection API usage.

Andrey Belyaev