BotProxy: Rotating Proxies Made for professionals. Really fast connection. Built-in IP rotation. Fresh IPs every day.
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.
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;
...
}