Demystifying Proxy Mechanism in Spring and Micronaut

Rafał Kosiński
bitso.engineering
Published in
6 min readJan 23, 2024

One crucial aspect of Bitso’s core operations is its Trading Engine, which primarily focuses on order matching and trade generation from a broad perspective. Order matching is the process of pairing buy and sell orders. It ensures efficient trade execution by finding matching orders at compatible prices. We recently had the opportunity to improve one of the Trade Engine’s components: the Orders Service. While enhancing order persistence functionality, we encountered an issue related to the @Retryable annotation in Micronaut. After the investigation, I discovered that the method on which @Retryable was used wasn’t proxied to add the retry feature.

My findings are quite interesting, as it would work just fine with the same configuration using the Spring framework. This inspired me to write this article expanding on the proxy mechanisms employed by Spring and Micronaut, revealing intriguing differences in their approaches.

Understanding the Proxy Mechanism

Proxies play a pivotal role in both Spring and Micronaut, facilitating features such as AOP (Aspect-Oriented Programming) and transaction management or retry mechanisms. Proxies act as intermediaries, allowing the frameworks to inject additional behavior or control over annotated components.

Interface-based proxy pattern example

Spring’s Proxy Mechanism

Spring supports two main types of proxies: JDK dynamic proxies and CGLIB proxies.

Those are the two most used proxy approaches in the Java ecosystem as you also might know. Here are some interesting points to observe:

JDK Dynamic Proxies

  • Creation at runtime: JDK dynamic proxies are created dynamically at runtime based on the interfaces implemented by the target class.
  • Interface-based: This type of proxy is interface-based, meaning that the target class must implement one or more interfaces to generate dynamic proxies.
  • Limitations: The main limitation is that JDK dynamic proxies can only proxy methods defined in interfaces.

CGLIB Proxies

  • Creation by code generation: CGLIB proxies are created through code generation at runtime by extending the target class.
  • Class-based: Unlike dynamic proxies, CGLIB proxies work with classes directly, allowing for the proxying of classes and methods not on interfaces.
  • Enhanced features: CGLIB proxies provide more flexibility but come with the cost of increased memory consumption and potential method overriding issues.
  • Limitations: CGLIB won’t work with final classes or methods as it cannot extend them.
Simplified representation of the CGLIB proxy

Understanding which type of proxy is in use is crucial for troubleshooting. If you encounter issues related to method visibility or unexpected behavior, it’s worth checking whether JDK dynamic or CGLIB proxies are employed.

More details on Spring proxy:
https://docs.spring.io/spring-framework/reference/core/aop/proxying.html

Micronaut’s Proxy Mechanism

In Micronaut, the proxy mechanism differs significantly from Spring’s runtime-oriented approach. Micronaut adopts a custom compile-time processing model, leveraging annotation processors during compilation to generate proxies. This approach provides certain advantages in terms of performance and enables Micronaut’s efficiency in resource utilization.

Compile-Time Processing

  • Proxies generated during compilation: Micronaut proxies are generated during compilation, allowing for efficient and optimized code.
  • Annotation processors: Micronaut relies on annotation processors to analyze annotated classes and generate the necessary proxy code.
  • Performance benefits: Since proxies are created at compile time, there is no runtime overhead associated with proxy creation, contributing to improved application performance in some areas(e.g., startup time).

More details on Micronaut proxy: https://docs.micronaut.io/latest/guide/index.html#how

Proxy difference example

Let’s check how Spring and Micronaut behave with similar configurations of retry features created by frameworks proxy.

The source code of the Spring example: https://github.com/rafal-kosinski/proxy-spring-example

The source code of the Micronaut example: https://github.com/rafal-kosinski/proxy-micronaut-example

In both projects there are classes:

  • RetrySingletonExample: A bean class created by annotation with the retryMe method demonstrating the use of the retry feature
RetrySingletonExample Micronaut’s version
  • RetryCreatedByFactoryExample: A bean class created by the RetryFactory class with the retryMe method demonstrating the use of the retry feature
RetryCreatedByFactoryExample Micronaut’s version
  • RetryExecutor: Executes retryMe in RetrySingletonExample and RetrySingletonExample once the application context loads
RetryExecutor Micronaut’s version
  • RetryFactory: Creates the RetryCreatedByFactoryExample bean
RetryFactory Micronaut’s version

Once you start the applications or run the test present in the linked project you will see the following output on the console:

Spring example:

Executing retryMe in class created by factory
Executing retryMe
Executing retryMe
Executing retryMe
Done
Executing retryMe in a class created using @Component
Executing retryMe
Executing retryMe
Executing retryMe
Done

Micronaut example:

Executing retryMe in a class created using @Singletion
Executing retryMe
Executing retryMe
Executing retryMe
Executing retryMe
Done
Executing retryMe in class created by factory
Executing retryMe
Done

You can see that the retryMe is retried for in both RetrySingletonExample and RetryCreatedByFactoryExample in the Spring example, which is not the case for the Micronaut example. In Micronaut, the retry works only for the bean created using an annotation. For the bean created in RetryFactory, RetryCreatedByFactoryExample, retry was not applied.

A closer look at proxies created for RetrySingletonExample

First, let’s see how RetrySingletonExample was proxied by frameworks to add the retry feature.

Spring:

CGLIB proxy in the stack trace

In the stack trace of the retryMe method originating from RetrySingletonExample, we can see that CGLIB is used to proxy the method call and add the retry feature.

Micronaut:

Micronaut proxy in the stack trace

The stack trace of calling the same method, that is retryMe, in the Micronaut example of the use of proxy generated during compilation that is adding the retry feature. Once the project is built, we can see this proxy class in the build artifacts:

Project tree depicting the proxy classes generated by Micronaut

Both frameworks created proxies to add the retry feature in their way and it works fine.

Why retries for RetryCreatedByFactoryExample does not work in Mcronaut but it works in Spring?

In the image shown above, we can spot classes with the $Intercepted suffix these classes intercept the calls to real objects like RetrySingletonExample adding extra features like retries, but we cannot spot such a class for RetryCreatedByFactoryExample. It means that this bean is not proxied, therefore no retry feature was added. Why is that?

Micronaut will not add a proxy for the object constructed by ourselves in the RetryFactory, where we are calling the constructor of RetryCreatedByFactoryExample as presented here:

@Factory
public class RetryFactory {
@Bean
RetryCreatedByFactoryExample retryCreatedByFactoryExample() {
return new RetryCreatedByFactoryExample();
}
}

In the Spring framework, the analogic configuration works, and the proxy is added on runtime using CGLIB similarly as it is for a bean created by annotation. That’s the use case where those two frameworks work differently for the same configuration because of their different approach to proxy objects.

Summary

To address the issue in the Orders Service mentioned at the beginning, where @Retryable was ineffective for a factory-created bean, we opted to use the @Singleton annotation. This adjustment successfully applied the retry proxy in Micronaut.

In summary, both Spring and Micronaut excel as frameworks, sharing similarities but differing in how they implement proxy mechanisms. The choice between them depends on your specific needs. Spring seems to provide more flexibility but comes with the cost of increased memory consumption and potential method overriding issues. Micronaut on the other hand emphasizes reduced memory consumption and optimized resource utilization for the cost of slightly limited proxy capability.

I hope this article is helpful. Thank you for taking the time to read it. If you could share, and comment, it would motivate me to create more content like this. I appreciate your honest feedback!

--

--