ryanp
ryanp

Reputation: 5127

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

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...

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

Answers (2)

ryanp
ryanp

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:

  1. register the infrastructure (caching, in this case) Advisors with your DefaultAdvisorAutoProxyCreator yourself; or to
  2. switch 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 CachingConfigurers 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

Indra Basak
Indra Basak

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,

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;

   ...

}

Upvotes: 3

Related Questions