使用带有自定义AOP建议的@EnableCaching时,代理类型(JDK与CGLIB)不匹配

时间:2017-11-01 13:59:17

标签: spring spring-aop

我一直试图让Spring的声明式缓存在一个应用程序中与一些自定义AOP建议一起工作,并且遇到了代理类型不匹配的问题。

给出以下Spring Boot应用程序主类:

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

和服务:

@Component
public class Service {

    @Cacheable(cacheNames = "int-strings")
    public String getString(int i) {
        return String.valueOf(i);
    }
}

我希望以下测试通过:

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

(此代码在https://github.com/hdpe/spring-cache-and-aop-issue上的可运行项目中都可用。)

但是,运行此测试会给出:

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

那为什么呢?好吧,我想 ......

  • @EnableCaching会触发创建InfrastructureAdvisorAutoProxyCreator,通过代理Service
  • 快乐地应用缓存建议
  • 由于Service不实现任何接口,因此CGLIB用于创建其代理
  • 我的DefaultAdvisorAutoProxyCreator然后运行以应用我的自定义建议(似乎,缓存建议再次)围绕服务方法
  • 由于该服务现在实际上是一个CGLIB代理,并且已经被Spring实现了SpringProxyAdvised接口,这次Spring创建了一个JDK动态代理
  • 动态代理不再是Service,因此自动装入测试类失败。

因此,为了解决问题(或者至少隐藏问题),我可以强制我的代理创建者生成CGLIB代理:

public DefaultAdvisorAutoProxyCreator proxyCreator() {
    DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
    proxyCreator.setProxyTargetClass(true);
    return proxyCreator;
}

我的测试然后通过,同样我可以测试声明性缓存也是可操作的。

所以我的问题:

这是解决此问题的最佳方法吗?让两个自动代理创建者适用于给定的bean是合法的还是个好主意?如果没有,那么使Spring的隐式自动代理创建者能够很好地使用自定义建议的最佳方法是什么?我怀疑“嵌套”代理是一个好主意,但无法理解如何覆盖@Enable*的隐式自动代理创建者。

2 个答案:

答案 0 :(得分:3)

如果bean使用@Transactional@Cacheable注释,则默认情况下Spring会生成 JDK动态代理以支持AOP。

动态代理类(com.sun.proxy.$Proxy61)继承/实现目标bean实现的所有接口。如果目标bean缺少接口,则代理类不实现接口。

但是,Spring框架可以使用cglib生成一个特殊的代理类(缺少一个接口),它继承自原始类并在子方法中添加行为。

由于您的Service类没有实现任何接口,因此Spring框架生成一个没有任何接口的合成代理类(com.sun.proxy。$ Proxy61)。

setProxyTargetClass中将true设置为DefaultAdvisorAutoProxyCreator后,Spring框架会使用cglib在运行时动态生成唯一的代理类。该类继承自Service类。代理命名模式通常类似于<bean class>$$EnhancerBySpringCGLIB$$<hex string>。例如,Service$$EnhancerBySpringCGLIB$$f3c18efe

在测试中,当BeanNotOfRequiredTypeException未设置为setProxyTargetClass时,Spring会抛出true,因为Spring找不到任何与您的Service类匹配的bean。

一旦使用cglib生成代理,您的测试就会停止失败,因为Spring会找到与您的Service类匹配的bean。

如果您为cglib课程引入了界面,则不必依赖Service。如果允许依赖于接口而不是实现,则可以进一步减少类之间的耦合。例如,

服务变更

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

ApplicationIT更改

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

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

   ...

}

答案 1 :(得分:2)

我花了很多时间来研究这个问题并取得了一些进展。

在考虑了它之后,我已经确定根本问题并不是Spring真的选择了错误的代理类型来包装已经代理的bean。 Spring正试图首先对bean进行双重代理!

TL; DR - 使用@AspectJ而不是DefaultAdvisorAutoProxyCreator

我遇到过的问题似乎是SPR-13990(DefaultAdvisorAutoProxyCreator doesn't get the target class of existing proxy - 已关闭,因为赢得修复)的表现形式。讨论后面的评论与我的用例特别相关。来自维护者:

  

Spring的AutoProxyCreators实际上总是创建一个新的代理......

SPR-6083(DefaultAdvisorAutoProxyCreator doesn't work with tx:annotation-driven on Cglib classes - 也赢得)也很有启发性;维护者说:

  

我们明确避免双重代理,但仅在使用隐式时   代理,例如通过<tx:annotation-driven><aop:config>。我&#39;米   害怕显式的DefaultAdvisorAutoProxyCreator定义   不会参与这个过程...

建议似乎是:

  1. 注册基础结构(在本例中为缓存)Advisor 你自己DefaultAdvisorAutoProxyCreator;或
  2. 切换到 &#34; <aop:config> style&#34;配置。
  3. 自己注册Advisor

    删除@EnableCaching并定义自己包含的bean,InfrastructureAdvisorAutoProxyCreator无法注册,我可以继续使用我自己的自动代理创建器,{{1}方法也将利用。我的配置如下所示:

    @Cacheable

    现在这非常可怕 - 我不得不毫无意义地重新定义了许多核心Spring配置,因此我可以关闭@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() { ... 并且我已经失去了使用{{1}的能力可能还有其他各种各样的东西。

    <强> @AspectJ

    所以第二种方法 - 一旦我发现&#34; AutoProxyRegistrar样式&#34;在JavaConfig中意味着@AspectJ注释 - 是要走的路。请注意,@ AspectJ在这里并不意味着您正在使用AspectJ编译器/编织器!它仍然只是Spring AOP从@AspectJ注释创建JDK或CGLIB代理。

    使用@ AspectJ-style配置,整个配置浓缩为:

    CachingConfigurer

    令人难以置信的是,这可行,因为<aop:config> 任何现有的@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(); } } } } 提升为@AspectJ自动代理创建者,导致只有一个代理同时应用我的自定义和缓存建议

    总结

    我认为如果你想使用@EnableAspectJAutoProxyInfrastructureAdvisorAutoProxyCreator等创建的隐式建议来使用自定义Spring AOP建议,你可能会更好地使用@AspectJ而不是{ {1}}。这似乎是春天希望你走下去的方向,他们暗示in the docs

      

    前一章描述了Spring对AOP的支持   @AspectJ和基于模式的方面定义。在本章中我们   通常讨论较低级别的Spring AOP API和AOP支持   用于Spring 1.2应用程序。对于新的应用程序,我们建议   使用Spring 2.0及以后的AOP支持描述   前一章......

    ......但是@EnableCaching似乎无处不在,我以为它是低级别的&#39;而不是春天1.2&#39;。