我一直在尝试让 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实现了
SpringProxy
和Advised
接口,这次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 定义 不会参与该过程...
这些建议似乎是:
- 注册基础结构(在本例中为缓存)
Advisor
你自己DefaultAdvisorAutoProxyCreator
;或到 - 切换到 "
<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
设置为true
inDefaultAdvisorAutoProxyCreator
,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;
...
}