Justin Wilson May 9, 2016
https://www.credera.com/blog/technology-insights/open-source-technology-insights/aspect-oriented-programming-in-spring-boot-part-2-spring-jdk-proxies-vs-cglib-vs-aspectj/
This is the second entry in a three-part series on aspect-oriented programming (AOP) with Spring Boot. This entry covers the differences between using AOP with Spring JDK proxies, CGLIB proxes, and AspectJ load-time weaving (LTW). The first entry covered how to write your own aspect, and the last entry will contain detailed AspectJ LTW setup information.
How Can You Call Code Without Writing Its Invocation?
The Spring framework supports two main methods of AOP—Spring proxies and AspectJ. Both methods allow code snippets called aspects to be injected or “woven” around existing code by targeting specific join points. However, even though both AOP styles can use the same code for their aspects, the way in which they weave those aspects into existing code is completely different.
Spring Proxies: JDK and CGLIB Styles
Every Spring bean (heretofore referred to as just a “bean”) can be considered a managed component. Whenever you declare a bean in XML or use @Component, @Service, or @Repository on a class targeted by Spring’s annotation component scanning (which is enabled by default for Spring Boot), that class is instantiated and managed as a singleton bean by the Spring framework. (It’s technically possible to use different scoping mechanisms to avoid singletons, but those won’t be covered here; see my blog series on dependency injection for more information.) Spring can inject references to that bean into other beans that request it using the @Autowired annotation (or @Resource or @Inject annotations) or beans that use “ref” in their XML definition. However, Spring doesn’t actually provide a literal reference to the original bean when it’s injected—it wraps the bean in a proxy class to give Spring a chance to weave in AOP code if needed.
There are two ways Spring can make proxies:
1. By default, Spring will try to use JDK dynamic proxy libraries to create a new instance of the injected bean’s interface which will act as a delegate to that bean. This behavior is demonstrated by the unit tests that use injected beans with interfaces in the spring-aop-proxy sample project.
2. If CGLIB is available on the classpath (which comes by default with Spring Boot) and there is no interface to implement a proxy with, CGLIB will be used to make a new object that extends the target bean’s class and acts as a delegate to that original bean. This behavior is also demonstrated by a unit test that uses an injected bean without an interface in the spring-aop-proxy sample project.
By default, Spring Boot will use JDK proxies if the bean has an interface and CGLIB proxies if the bean does not. If you try to inject a bean using its concrete class and that bean has an interface, Spring will fail to find the bean with a NoSuchBeanDefinitionException because it will fail to create a JDK proxy using the concrete class. In the first example above, that would be using this:
|
@Autowired
private ImplForInterface bean;
Instead of this:
@Autowired
private Interface bean;
To get around this limitation and be able to inject concrete classes that have interfaces, you have to tell Spring to always use CGLIB for all beans. This will disable Spring’s use of JDK proxies entirely, making Spring always extend concrete classes even if an interface is injected. To enable this in Spring Boot, just add the following Java property, as seen in the spring-aop-proxy-cglib sample project’s application.properties file:
spring.aop.proxy-target-class=true
Spring Proxies: Annotations on Interfaces
Consider an aspect that’s used to weave code around annotated methods (such as the HystrixAspect that targets @HystrixWrapper annotated methods from part one in this series).
- When a Spring JDK proxy is used, the join point annotation should be present on both the interface’s method and the concrete class’s method for the aspect to trigger correctly.
- The second to last test in the spring-aop-proxy test class proves that both the interface and the concrete class require the join point annotation when a JDK proxy is used.
- When a CGLIB proxy is used, the concrete implementation must have the annotation on its method for the aspect to trigger.
- The second to last test in the spring-aop-proxy-cglib test class proves that the interface’s join point annotations are ignored when CGLIB is used.
The JDK proxy unit test that expects an InaccessablePointcutAnnotationException is of particular interest. It demonstrates that if you’re using a JDK proxy and the join point annotation is present on the concrete implementation of a bean and not on its interface, the aspect will still trigger but it won’t be able to find the annotation that was used to trigger it. This prevents us from finding the Hystrix command group key that should come from the wrapped method’s annotation, as shown in the bold parts of the example below. (This is the GitHub link for the @HystrixWrapper annotation, and this is the link for the HystrixAspect.)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HystrixWrapper {
public String commandGroupKey();
}
@Aspect
@Component
public class HystrixAspect {
@Around("within(org.jdw.blog..*) && " "@annotation(org.jdw.blog.common.annotation.HystrixWrapper)")
public Object around(final ProceedingJoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
HystrixWrapper annotation = method.getAnnotation(HystrixWrapper.class);
if (annotation == null) { // Can occur when not using CCGLIB-style 'subclass' proxies,
// when using an interface that has a concrete class
// that has the annotation.
throw new InaccessablePointcutAnnotationException(); }
// Hystrix invocation code goes here, see GitHub for details.
// [https://github.com/jwilsoncredera/spring-aop-blog/blob/master/spring-aop-common/src/main/java/org/jdw/blog/common/aspect/HystrixAdvice.java](https://github.com/jwilsoncredera/spring-aop-blog/blob/master/spring-aop-common/src/main/java/org/jdw/blog/common/aspect/HystrixAdvice.java)
}
}
I recommend always using CGLIB to avoid this edge case. Doing so makes all Spring proxy behavior consistent since JDK proxies will never be used, it allows the injection of concrete implementations of beans which can be useful for unit tests, and it negates the need to duplicate join point annotations on interfaces and concrete classes (which could easily get out of sync). As a reminder, to always use CGLIB, just set the “spring.aop.proxy-target-class” property to true.
Before using CGLIB, ensure your codebase always uses pre-existing AOP annotations (such as @Transactional) on concrete classes instead of only on interfaces. Interface-only AOP annotations will be ignored when CGLIB is enabled. Changing when @Transactional aspects are triggered could lead to items not being saved to the database, or poor performance due to transactional boundary shifting.
Spring Proxies: Annotations in Concrete Classes
Both JDK- and CGLIB-style Spring proxies have one glaring weakness—AOP can only be invoked by calling a method on an injected bean. Consider a bean with these two methods (all of the classes in this package qualify):
@HystrixWrapper(commandGroupKey = "blog")
public long hystrixWrappedGetCurrentThreadId() {
return getCurrentThreadId();
}
public long nestedHystrixWrappedGetCurrentThreadId() {
return hystrixWrappedGetCurrentThreadId();
}
The second method cannot trigger the HystrixAspect when calling the first method because it invokes the first method directly without going through a Spring proxy—the bean is just calling one of its own methods. Spring doesn’t have any opportunities to inject the aspect code. This is demonstrated by every other test in both the spring-aop-proxy test class and spring-aop-proxy-cglib test class (all the tests that start with “testNestedHystrixWrappedMethod”).
However, when AspectJ is used instead of Spring proxies, the HystrixAspect does trigger when a bean calls its own join point annotated method. This is shown by every other test in the spring-aop-aspectj-ltw test class (which also begin with “testNestedHystrixWrappedMethod”)—every scenario that fails to invoke the Hystrix aspect for Spring proxies succeeds for AspectJ (except for one specific scenario which will be covered later). How can this be?
AspectJ
AspectJ extends the Java compiler to weave aspect code directly into join points. It doesn’t require Spring to run. Instead, you need to use JVM arguments to enable a compiler extension along with an aop.xml file and another configuration file which will be covered in part three of this series.
This has two important ramifications. First, if you’re using AspectJ, you don’t need to inject Spring beans to invoke AOP functionality. Technically you don’t even need Spring beans for AOP at all when AspectJ is enabled—you could manually create a new object and invoke AOP code on it right away if it had join points your aspects could target.
Second, if an object calls its own methods it can trigger AOP code, unlike AOP using Spring proxies. Here’s a visual example of how this AspectJ behavior would look if the @HystrixAspect used @Before instead of @Around to inject code in front of its join point targets instead of completely wrapping them (because it’s easier to depict):
The fact that beans can trigger their own AOP code means you need to be very careful when enabling AspectJ on existing Spring projects, because this can change when database transactions are started by @Transactional annotations. For example, if you had a bean with two methods and one didn’t have an @Transactional annotation and it called another one that did, then that invocation would start a database transaction with AspectJ enabled but not with default Spring proxy-based AOP.
public void notAnnotated() {
annotated();
}
@Transactional
public void annotated() {
// With AspectJ, a direct call from notAnnotated will enable the transaction.
// Without it, this method will only get a transaction if it was called from a Spring proxy.
}
Luckily, there is a way around this issue—AspectJ and Spring proxies can co-exist. The aop.xml file lets you decide which aspects will be managed by AspectJ and which packages are included or excluded from AspectJ code weaving. If you exclude classes that use @Transactional from AspectJ weaving, their behavior will default to the same Spring proxy behavior they were using before. Stay tuned for part three of this series, which covers how to configure an aop.xml file.
What About That Unit Test Where AspectJ Didn’t Work?
As noted earlier, every other test in the spring-aop-aspectj unit test (which each begin with “testNestedHystrixWrappedMethod”) demonstrates how beans can trigger the HystrixAspect when calling their own @HystrixWrapper join point annotated methods without going through Spring beans… except for the last test.
That last test invokes the “nestedGetCurrentThreadId” method on a Spring CGLIB proxy of a class similar to the example below (GitHub link for the original class). The class’s interface annotates “getCurrentThreadId” with @HystrixWrapper, but the concrete class itself does not. Thus, when “getCurrentThreadId” is invoked by “nestedGetCurrentThreadId” within the CGLIB proxy, AspectJ looks at the concrete class instead of the interface and can’t find the @HystrixWrapper join point. Therefore, the HystrixAspect is not woven.
@Override
public long getCurrentThreadId () {
return Thread.currentThread().getId();
}
@Override
public long nestedGetCurrentThreadId () {
return getCurrentThreadId();
}
The takeaway from this scenario is that you should always put join point annotations on concrete classes when using AspectJ, just like you should when using Spring CGLIB proxies for AOP.
Next Steps
Stay tuned for the third and final entry in this blog series, which will cover how to configure AspectJ for Spring Boot. It contains a critical piece of JVM configuration information that isn’t widely found in other tutorials (including Spring’s own documentation), so be sure to catch the next entry by following @CrederaOpen on Twitter or connecting with us on LinkedIn.