Reputation: 5127
I have been trying to get Spring's declarative caching working in an application alongside some custom AOP advice, and have hit an issue with mismatched proxy types.
Given the following Spring Boot application main class:
@SpringBootApplication
@EnableCaching
public class Application {
@Bean
public DefaultAdvisorAutoProxyCreator proxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
@Bean
public NameMatchMethodPointcutAdvisor pointcutAdvisor() {
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setClassFilter(new RootClassFilter(Service.class));
advisor.addMethodName("*");
advisor.setAdvice(new EnsureNonNegativeAdvice());
return advisor;
}
public static class EnsureNonNegativeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target)
throws Throwable {
if ((int) args[0] < 0) {
throw new IllegalArgumentException();
}
}
}
}
and service:
@Component
public class Service {
@Cacheable(cacheNames = "int-strings")
public String getString(int i) {
return String.valueOf(i);
}
}
I would expect the following test to pass:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class ApplicationIT {
@Autowired
private Service service;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void getStringWithNegativeThrowsException() {
thrown.expect(IllegalArgumentException.class);
service.getString(-1);
}
}
(This code is all available in a runnable project on https://github.com/hdpe/spring-cache-and-aop-issue).
However, running this test gives:
org.springframework.beans.factory.UnsatisfiedDependencyException:
...<snip>...
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'service' is expected to be of type 'me.hdpe.spring.cacheandaop.Service' but was actually of type 'com.sun.proxy.$Proxy61'
at org.springframework.beans.factory.support.DefaultListableBeanFactory.checkBeanNotOfRequiredType(DefaultListableBeanFactory.java:1520)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1498)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1099)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1060)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:578)
...
So why is this? Well, I think...
@EnableCaching
triggers the creation of an InfrastructureAdvisorAutoProxyCreator
, which will happily apply the cache advice via a proxy around Service
Service
implements no interfaces, CGLIB is used to create its proxyDefaultAdvisorAutoProxyCreator
then runs to apply my custom advice (and, it seems, the cache advice again) around the service methodSpringProxy
and Advised
interfaces by Spring, this time Spring creates a JDK dynamic proxyService
, and so autowiring into the test class fails.So to fix the problem (or, at least, hide this problem) I can force my proxy creator to generate CGLIB proxies:
public DefaultAdvisorAutoProxyCreator proxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
My test then passes, and similarly I can test that the declarative caching is also operational.
So my question(s):
Is this the best way to fix this problem? Is it legal, or a good idea, to have two auto proxy creators applicable to a given bean? And if not, what is the best way to make Spring's implicit auto proxy creators play nicely with custom advice? I'm suspicious that "nested" proxies are a good idea, but can't work out how to override @Enable*
's implicit auto proxy creators.
Upvotes: 4
Views: 4918
Reputation: 5127
I've spent an enormous amount of time looking at this problem and have made a little bit of headway.
Having thought about it some more, I've decided the root problem is not really that Spring is choosing the wrong type of proxy to wrap around an already-proxied bean. It's that Spring is trying to double-proxy a bean in the first place!
TL;DR - use @AspectJ instead of DefaultAdvisorAutoProxyCreator
The issue as I've encountered it appears to be a manifestation of SPR-13990 (DefaultAdvisorAutoProxyCreator doesn't get the target class of existing proxy - closed as Won't Fix). The comments later in the discussion are particularly pertinent to my use case. From the maintainer:
Spring's AutoProxyCreators actually always create a new proxy...
SPR-6083 (DefaultAdvisorAutoProxyCreator doesn't work with tx:annotation-driven on Cglib classes - also Won't Fix) is also illuminating; the maintainer says:
We do explicitly avoid double proxying, but only when using implicit proxies such as through
<tx:annotation-driven>
or<aop:config>
. I'm afraid that an explicit DefaultAdvisorAutoProxyCreator definition won't participate in that process...
The suggestions seem to be to:
Advisor
s with
your DefaultAdvisorAutoProxyCreator
yourself; or to <aop:config>
style" configuration.Registering the Advisor
yourself
By removing @EnableCaching
and defining the beans it contains myself, the InfrastructureAdvisorAutoProxyCreator
won't be registered, and I can continue to use my own auto-proxy creator, which my @Cacheable
methods will also leverage. My configuration then looks like this:
@SpringBootApplication
public class Application {
@Bean
public DefaultAdvisorAutoProxyCreator proxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
@Bean
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource();
}
@Bean
public CacheInterceptor cacheInterceptor() {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.setCacheOperationSources(cacheOperationSource());
return interceptor;
}
@Bean
public BeanFactoryCacheOperationSourceAdvisor cacheOperationSourceAdvisor() {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setAdvice(cacheInterceptor());
advisor.setCacheOperationSource(cacheOperationSource());
return advisor;
}
@Bean
public NameMatchMethodPointcutAdvisor pointcutAdvisor() {
...
Now this is pretty horrible - I've had to pointlessly redefine lots of core Spring configuration just so I can turn the AutoProxyRegistrar
off and I've lost the ability to use CachingConfigurer
s and probably all sorts of other things.
@AspectJ
So the second approach - once I figured out that "<aop:config>
style" in JavaConfig meant @AspectJ annotations - is the way to go. Note that @AspectJ here doesn't mean you're using the AspectJ compiler/weaver! It's still just Spring AOP creating JDK or CGLIB proxies from the @AspectJ annotations.
With @AspectJ-style config, the whole configuration condenses to this:
@SpringBootApplication
@EnableCaching
@EnableAspectJAutoProxy
public class Application {
@Aspect
@Component
public static class EnsureNonNegativeAspect {
@Before("execution(* me.hdpe.spring.cacheandaop.Service.*(..)) && args(i)")
public void ensureNonNegative(int i) {
if (i < 0) {
throw new IllegalArgumentException();
}
}
}
}
Incredibly this works because @EnableAspectJAutoProxy
promotes any existing InfrastructureAdvisorAutoProxyCreator
to an @AspectJ auto-proxy creator, resulting in just a single proxy applying both my custom and the caching advice.
So in summary
I think if you want to use custom Spring AOP advice with the implicit advice created by @EnableCaching
, @EnableTransactionManagement
etc., you're probably much better off using @AspectJ rather than a DefaultAdvisorAutoProxyCreator
. This seems to be the direction Spring want you to go down and they hint as much in the docs:
The previous chapter described the Spring's support for AOP using @AspectJ and schema-based aspect definitions. In this chapter we discuss the lower-level Spring AOP APIs and the AOP support typically used in Spring 1.2 applications. For new applications, we recommend the use of the Spring 2.0 and later AOP support described in the previous chapter...
...but the DefaultAdvisorAutoProxyCreator
seemed sufficiently ubiquitous that I imagined it was 'lower-level' rather than 'Spring 1.2'.
Upvotes: 4
Reputation: 7394
If a bean uses @Transactional
or @Cacheable
annotation, the Spring generates JDK Dynamic proxies by default to support AOP.
A dynamic proxy classes (com.sun.proxy.$Proxy61
) inherits/implements all the interfaces that the target bean implements. The proxy class doesn't implement an interface if the target bean is missing an interface.
However, the Spring framework can use cglib
to generate a special proxy class (which is missing an interface) that inherits from the original class and adds the behavior in the child methods.
Since your Service
class doesn't implement any interface, the Spring framework generates a synthetic proxy class (com.sun.proxy.$Proxy61) without any interface.
Once you set the setProxyTargetClass
to true
in DefaultAdvisorAutoProxyCreator
, the Spring framework generates a unique proxy class dynamically at runtime using cglib
. This class inherits from Service
class. The proxy naming pattern usually resembles <bean class>$$EnhancerBySpringCGLIB$$<hex string>
. For example, Service$$EnhancerBySpringCGLIB$$f3c18efe
.
In your test, Spring throws a BeanNotOfRequiredTypeException
when the setProxyTargetClass
is not set to true
since Spring doesn't find any bean which matches your Service
class.
Your test stops failing once you generate the proxy with cglib
as Spring finds a bean matching your Service
class.
You don't have to depend on cglib
, if you introduce an interface for your Service
class. You can further reduce coupling between classes if you allow dependency on interfaces instead of the implementations. For example,
public interface ServiceInterface {
String getString(int i);
}
@Component
public class Service implements ServiceInterface {
@Cacheable(cacheNames = "int-strings")
public String getString(int i) {
return String.valueOf(i);
}
}
@Bean
public NameMatchMethodPointcutAdvisor pointcutAdvisor() {
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
//advisor.setClassFilter(new RootClassFilter(Service.class));
advisor.setClassFilter(new RootClassFilter(ServiceInterface.class));
advisor.addMethodName("*");
advisor.setAdvice(new EnsureNonNegativeAdvice());
return advisor;
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class ApplicationIT {
@Autowired
//private Service service;
private ServiceInterface service;
...
}
Upvotes: 3