Need Proxy?

BotProxy: Rotating Proxies Made for professionals. Really fast connection. Built-in IP rotation. Fresh IPs every day.

Find out more


Mismatched proxy types (JDK vs CGLIB) when using @EnableCaching with custom AOP advice

Question

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
  • As Service implements no interfaces, CGLIB is used to create its proxy
  • My DefaultAdvisorAutoProxyCreator then runs to apply my custom advice (and, it seems, the cache advice again) around the service method
  • As the service is now actually a CGLIB proxy, and has been made to implement the SpringProxy and Advised interfaces by Spring, this time Spring creates a JDK dynamic proxy
  • The dynamic proxy is no longer a Service, 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.

Answer

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,

Service Changes

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);
    }
}

Application Class

@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;
}

ApplicationIT Changes

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class ApplicationIT {

    @Autowired
    //private Service service;
    private ServiceInterface service;

   ...

}

cc by-sa 3.0