将@EnableCaching与自定义 AOP 建议一起使用时代理类型不匹配(JDK 与 CGLIB)



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

给定以下 Spring 引导应用程序主类:

@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*的隐式自动代理创建者。

我花了大量的时间研究这个问题,并取得了一些进展。

经过进一步思考,我决定根本问题并不是 Spring 选择了错误的代理类型来包裹已经代理的 bean。而是春天一开始就试图双重代理豆子!

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

我遇到的问题似乎是 SPR-13990 的表现形式(DefaultAdvisorAutoProxyCreator 没有获得现有代理的目标类 - 由于无法修复而关闭)。讨论后面的评论与我的用例特别相关。来自维护者:

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

SPR-6083(DefaultAdvisorAutoProxyCreator 不适用于 Cglib 类上的 tx:annotation-driven - 也不会修复)也很有启发性;维护者说:

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

这些建议似乎是:

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

自行注册Advisor

通过删除@EnableCaching并自己定义它包含的 bean,InfrastructureAdvisorAutoProxyCreator不会被注册,我可以继续使用我自己的自动代理创建器,我的@Cacheable方法也将利用它。然后我的配置如下所示:

@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() {
...

现在这非常可怕 - 我不得不毫无意义地重新定义许多核心 Spring 配置,以便我可以关闭AutoProxyRegistrar,我已经失去了使用CachingConfigurer的能力,可能还有各种其他东西。

@AspectJ

因此,第二种方法 - 一旦我发现JavaConfig中的"<aop:config>风格"意味着@AspectJ注释 - 就是要走的路。请注意,@AspectJ这里并不意味着您正在使用AspectJ编译器/编织者!它仍然只是Spring AOP从@AspectJ注释中创建JDK或CGLIB代理。

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

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

令人难以置信的是,这是有效的,因为@EnableAspectJAutoProxy将任何现有InfrastructureAdvisorAutoProxyCreator提升为@AspectJ自动代理创建者,导致只有一个代理同时应用我的自定义和缓存建议。

所以总结一下

我认为如果您想将自定义的 Spring AOP 建议与@EnableCaching@EnableTransactionManagement等创建的隐式建议一起使用,您可能最好使用 @AspectJ 而不是DefaultAdvisorAutoProxyCreator。这似乎是Spring希望你走的方向,他们在文档中暗示了很多:

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

。但是DefaultAdvisorAutoProxyCreator似乎无处不在,以至于我认为它是"较低级别的"而不是"Spring 1.2"。

如果 Bean 使用@Transactional@Cacheable注释,Spring 默认生成JDK 动态代理以支持 AOP。

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

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

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

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

在您的测试中,当setProxyTargetClass未设置为true时,Spring 会抛出一个BeanNotOfRequiredTypeException,因为 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;
}

应用程序 IT 更改

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class ApplicationIT {
@Autowired
//private Service service;
private ServiceInterface service;
...
}

最新更新