使用带有自定义 AOP 建议的 @EnableCaching 时代理类型不匹配(JDK vs CGLIB)
Mismatched proxy types (JDK vs CGLIB) when using @EnableCaching with custom AOP advice
我一直在尝试让 Spring 的声明式缓存与一些自定义 AOP 建议一起在应用程序中工作,但遇到了代理类型不匹配的问题。
给定以下 Spring 引导应用程序主程序 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();
}
}
}
}
和服务:
@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
,因此自动连接到测试 class 失败。
所以要解决这个问题(或者,至少,隐藏这个问题)我可以强制我的代理创建者生成 CGLIB 代理:
public DefaultAdvisorAutoProxyCreator proxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
然后我的测试通过了,同样我可以测试声明式缓存也可以运行。
所以我的问题:
这是解决此问题的最佳方法吗?有两个适用于给定 bean 的自动代理创建者是合法的还是一个好主意?如果不是,让 Spring 的隐式自动代理创建者与自定义建议很好地配合的最佳方法是什么?我怀疑 "nested" 代理是个好主意,但不知道如何覆盖 @Enable*
的隐式自动代理创建者。
如果 bean 使用 @Transactional
或 @Cacheable
注解,Spring 默认生成 JDK 动态代理 以支持AOP.
动态代理 classes (com.sun.proxy.$Proxy61
) inherits/implements 目标 bean 实现的所有接口。如果目标 bean 缺少接口,则代理 class 不会实现接口。
然而,Spring 框架可以使用 cglib
生成一个特殊的代理 class(它缺少一个接口),它继承了原始的 class 并添加了子方法中的行为。
由于您的 Service
class 没有实现任何接口,Spring 框架生成一个合成代理 class (com.sun.proxy.$Proxy61)没有任何界面。
在 DefaultAdvisorAutoProxyCreator
中将 setProxyTargetClass
设置为 true
后,Spring 框架会在运行时使用 [=16] 动态生成唯一的代理 class =].此 class 继承自 Service
class。代理命名模式通常类似于 <bean class>$$EnhancerBySpringCGLIB$$<hex string>
。例如,Service$$EnhancerBySpringCGLIB$$f3c18efe
。
在您的测试中,Spring 在 setProxyTargetClass
未设置为 true
时抛出 BeanNotOfRequiredTypeException
,因为 Spring 找不到任何 bean匹配您的 Service
class。
一旦您使用 cglib
生成代理,您的测试就会停止失败,因为 Spring 找到一个与您的 Service
class.
匹配的 bean
如果您为 Service
class 引入接口,则不必依赖 cglib
。如果允许依赖于接口而不是实现,则可以进一步减少 classes 之间的耦合。例如,
服务变更
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);
}
}
申请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 变化
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class ApplicationIT {
@Autowired
//private Service service;
private ServiceInterface service;
...
}
我花了很多时间研究这个问题,并取得了一些进展。
再考虑一下后,我认为根本问题不在于 Spring 选择了错误的代理类型来包裹已被代理的 bean。就是 Spring 首先试图对一个 bean 进行双重代理!
TL;DR - 使用@AspectJ 而不是 DefaultAdvisorAutoProxyCreator
我遇到的问题似乎是 SPR-13990 的表现(DefaultAdvisorAutoProxyCreator doesn't get the target class of existing proxy - 关闭为 不会修复)。讨论后面的评论与我的用例特别相关。来自维护者:
Spring's AutoProxyCreators actually always create a new proxy...
SPR-6083(DefaultAdvisorAutoProxyCreator doesn't work with tx:annotation-driven on Cglib classes - 也不会修复)也很有启发性;维护者说:
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...
建议似乎是:
- 注册基础架构(在本例中为缓存)
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
s,可能还有其他各种东西。
@AspectJ
所以第二种方法——一旦我发现 JavaConfig 中的“<aop:config>
风格”意味着 @AspectJ 注释——就是可行的方法。请注意,这里的@AspectJ 并不意味着您正在使用 AspectJ compiler/weaver!它仍然只是 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 要你往下走的方向,他们也暗示了 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...
...但是 DefaultAdvisorAutoProxyCreator
似乎无处不在,我认为它是 'lower-level' 而不是 'Spring 1.2'。
我一直在尝试让 Spring 的声明式缓存与一些自定义 AOP 建议一起在应用程序中工作,但遇到了代理类型不匹配的问题。
给定以下 Spring 引导应用程序主程序 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();
}
}
}
}
和服务:
@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
,因此自动连接到测试 class 失败。
所以要解决这个问题(或者,至少,隐藏这个问题)我可以强制我的代理创建者生成 CGLIB 代理:
public DefaultAdvisorAutoProxyCreator proxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
然后我的测试通过了,同样我可以测试声明式缓存也可以运行。
所以我的问题:
这是解决此问题的最佳方法吗?有两个适用于给定 bean 的自动代理创建者是合法的还是一个好主意?如果不是,让 Spring 的隐式自动代理创建者与自定义建议很好地配合的最佳方法是什么?我怀疑 "nested" 代理是个好主意,但不知道如何覆盖 @Enable*
的隐式自动代理创建者。
如果 bean 使用 @Transactional
或 @Cacheable
注解,Spring 默认生成 JDK 动态代理 以支持AOP.
动态代理 classes (com.sun.proxy.$Proxy61
) inherits/implements 目标 bean 实现的所有接口。如果目标 bean 缺少接口,则代理 class 不会实现接口。
然而,Spring 框架可以使用 cglib
生成一个特殊的代理 class(它缺少一个接口),它继承了原始的 class 并添加了子方法中的行为。
由于您的 Service
class 没有实现任何接口,Spring 框架生成一个合成代理 class (com.sun.proxy.$Proxy61)没有任何界面。
在 DefaultAdvisorAutoProxyCreator
中将 setProxyTargetClass
设置为 true
后,Spring 框架会在运行时使用 [=16] 动态生成唯一的代理 class =].此 class 继承自 Service
class。代理命名模式通常类似于 <bean class>$$EnhancerBySpringCGLIB$$<hex string>
。例如,Service$$EnhancerBySpringCGLIB$$f3c18efe
。
在您的测试中,Spring 在 setProxyTargetClass
未设置为 true
时抛出 BeanNotOfRequiredTypeException
,因为 Spring 找不到任何 bean匹配您的 Service
class。
一旦您使用 cglib
生成代理,您的测试就会停止失败,因为 Spring 找到一个与您的 Service
class.
如果您为 Service
class 引入接口,则不必依赖 cglib
。如果允许依赖于接口而不是实现,则可以进一步减少 classes 之间的耦合。例如,
服务变更
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);
}
}
申请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 变化
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
public class ApplicationIT {
@Autowired
//private Service service;
private ServiceInterface service;
...
}
我花了很多时间研究这个问题,并取得了一些进展。
再考虑一下后,我认为根本问题不在于 Spring 选择了错误的代理类型来包裹已被代理的 bean。就是 Spring 首先试图对一个 bean 进行双重代理!
TL;DR - 使用@AspectJ 而不是 DefaultAdvisorAutoProxyCreator
我遇到的问题似乎是 SPR-13990 的表现(DefaultAdvisorAutoProxyCreator doesn't get the target class of existing proxy - 关闭为 不会修复)。讨论后面的评论与我的用例特别相关。来自维护者:
Spring's AutoProxyCreators actually always create a new proxy...
SPR-6083(DefaultAdvisorAutoProxyCreator doesn't work with tx:annotation-driven on Cglib classes - 也不会修复)也很有启发性;维护者说:
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...
建议似乎是:
- 注册基础架构(在本例中为缓存)
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
s,可能还有其他各种东西。
@AspectJ
所以第二种方法——一旦我发现 JavaConfig 中的“<aop:config>
风格”意味着 @AspectJ 注释——就是可行的方法。请注意,这里的@AspectJ 并不意味着您正在使用 AspectJ compiler/weaver!它仍然只是 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 要你往下走的方向,他们也暗示了 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...
...但是 DefaultAdvisorAutoProxyCreator
似乎无处不在,我认为它是 'lower-level' 而不是 'Spring 1.2'。