DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • How To Build Web Service Using Spring Boot 2.x
  • Aggregating REST APIs Calls Using Apache Camel
  • Introduction to Apache Kafka With Spring
  • Dependency Injection in Spring

Trending

  • Event Driven Architecture (EDA) - Optimizer or Complicator
  • How To Introduce a New API Quickly Using Quarkus and ChatGPT
  • Code Reviews: Building an AI-Powered GitHub Integration
  • Apple and Anthropic Partner on AI-Powered Vibe-Coding Tool – Public Release TBD
  1. DZone
  2. Coding
  3. Frameworks
  4. Think Twice Before Using Reflection

Think Twice Before Using Reflection

Replacing reflection API calls can improve performance.

By 
Andrey Belyaev user avatar
Andrey Belyaev
DZone Core CORE ·
Apr. 09, 19 · Presentation
Likes (9)
Comment
Save
Tweet
Share
41.1K Views

Join the DZone community and get the full member experience.

Join For Free

Image title

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 that's name is specified as an annotation property. In such cases, you always have an answer: use reflection!

In the new version of the CUBA framework, we decided to improve many aspects of the architecture and one of the most significant changes 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 improvements 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 compilation and code generation and LambdaMetafactory.

Reflection: Good, Old Reliable API

According to Wikipedia:

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

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, the 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 of 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 near future; mature frameworks are big and complex and used in many mission-critical systems, and because of this, 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 is 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. It looks very promising, but are there any trade-offs here? And the answer is: yes.

First off, you can run code that is not exactly yours. Code generation changes your original code; therefore, if something goes wrong, you cannot tell whether it is your mistake or 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 is that 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 an 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, we were introduced to a new JVM instruction — invokedynamic . Initially targeted at dynamic language implementations based on the 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 a further development of dynamic invocation idea. This API is based on MethodHandle.

The MethodHandle API is a good replacement for a 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 when compared to the 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 an 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 the 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, but the functionality is the same. Additionally, 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 the 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 Value Throughput (ops/us) Execution Time (us/op)
LambdaGetTest 72 0.0118
ReflectionGetTest 65 0.0177
DirectMethodGetTest 260 0.0048
Test - Set Value Throughput (ops/us) Execution Time (us/op)
LambdaSetTest 96 0.0092
ReflectionSetTest 58 0.0173
DirectMethodSetTest 415 0.0031

As you can see, the lambda-based method handlers are about 30 percent faster on average. It is a good discussion here regarding lambda-based method invocation performance. The outcome was that 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. however, it was sufficient for our purposes.

Implementation

In CUBA, you can use the @Subscribe annotation to make a method to “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 the 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 is 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 the data component into the controller — the framework will find it by component ID.

Conclusion

Despite the recent introduction of the new generation of the frameworks (Micronaut, Quarkus) that has 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 near 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. 

Framework API Java (programming language) Spring Framework application Event

Published at DZone with permission of Andrey Belyaev. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How To Build Web Service Using Spring Boot 2.x
  • Aggregating REST APIs Calls Using Apache Camel
  • Introduction to Apache Kafka With Spring
  • Dependency Injection in Spring

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!