SpringAop 代理模式及 AopContext 问题小记

AOP 称为面向切面编程,其底层原理就是动态代理;JAVA 中比较常见的动态代理有两种,分别是 JDK 动态代理和 CGLIB 动态代理,这点从 Spring Aop 的 AopProxy 的实现就可以得出验证。

1
2
3
AopProxy
--- JdkDynamicAopProxy
--- CglibAopProxy

Spring 作为 Java 应用领域最牛 X 的基础框架产品,在对于一些版本变更导致的兼容性问题的处理上一直被诟病,对于这两种代理方式的选择上,Spring 不同版本存在一定的差异,这也是本文产生的一个原因。

从一个异常说起

本来是打算写个 AOP demo 来验证下 AopContext 在跨线程场景下丢失 proxy 对象问题的,由于在 pom 中指定依赖了 spring-aop 版本(5.1.2),而工程 spring-boot 版本使用是 2.4.2,启动时就抛出了如下的异常(仅截取了后 3 段 casuse by):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdkServiceImpl' defined in file [/Users/sgl/Documents/projects/github/aop-guides/target/classes/com/glmapper/bridge/boot/jdk/JdkServiceImpl.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Unexpected AOP exception; nested exception is java.lang.IllegalStateException: Unable to load cache item
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:617)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1380)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)
... 46 more
Caused by: org.springframework.aop.framework.AopConfigException: Unexpected AOP exception; nested exception is java.lang.IllegalStateException: Unable to load cache item
at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:214)
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:473)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:352)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:301)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:444)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1792)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:609)
... 55 more
Caused by: java.lang.IllegalStateException: Unable to load cache item
at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572)
at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:419)
at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:58)
at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:205)
... 62 more
Caused by: java.lang.VerifyError: Stack map does not match the one at exception handler 9
Exception Details:
Location:
com/glmapper/bridge/boot/jdk/JdkServiceImpl$$EnhancerBySpringCGLIB$$3045df64.<init>()V @9: athrow
Reason:
Current frame's flags are not assignable to stack map frame's.
Current Frame:
bci: @0
flags: { flagThisUninit }
locals: { uninitializedThis }
stack: { 'java/lang/RuntimeException' }
Stackmap Frame:
bci: @9
flags: { }
locals: { }
stack: { 'java/lang/Throwable' }

看到这个异常有点莫名其妙,然后尝试通过关键字去网上搜了下,确实找到了一个非常类似的 case Unexpected AOP exception; nested exception is java.lang.IllegalStateException: Unable to load cache item。从问题描述来看,大致有以下几个方向:

  • 1、spring boot devtools 热部署导致
  • 2、CGLIB 或 Objenesis 不理解 Java 13 字节代码
  • 3、依赖问题

在我的 demo 中,1 和 2 都是未涉及的,所以将专注点放在 3 上面。一开始尝试 Stack map does not match the one at exception handler 9 全局查找,尝试找到产生堆栈的代码片段,实际上是没有在代码中找到的;随后翻看了 openjdk 的源码才找到。另一个比较疑惑的问题在于:这个异常产生仅当通过 run test unit 的时候才会有,正常通过启动工程并发起调用并不会出现。当我把 Spring Aop 版本切换到 5.3.3 时,run test unit 也可以正常被执行。

图1:spring-aop 版本为 5.3.3, Class.forName 正常执行到下一行

图2:spring-aop 版本为 5.1.2

执行直接报错

其实堆栈来看,两者并无差异。到这里不打算继续深究原因,回到 VerifyError 异常:

当 “verifier” 检测到 classfile 虽然格式良好,但包含某种内部不一致或安全问题时抛出。

加上产生的条件:在 springboot 测试场景下,使用低于管控版本的 spring aop 从而导致该问题;合理规避吧…

Spring aop 动态代理机制在不同版本中的差异

这里仅作为备忘点介绍一下,大多数情况下在使用的时候并不会关注到。

  • 1、Spring 5.x 版本开始,AOP 默认依旧使用 JDK 动态代理,并非网上说的默认改成了 CGLIB。
  • 2、SpringBoot 2.x 开始,由于使用 JDK 动态代理可能导致的类型转化异常问题,默认使用 CGLIB。
  • 3、SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过配置项 spring.aop.proxy-target-class=false 来进行修改,proxyTargetClass 配置已无效。

相关论证可以见:AopAutoConfiguration、@EnableAspectJAutoProxy 及官方文档说明。

AopContext

关于 AopContext 可能部分开发者对其是陌生的;但是有这样一种场景一定是你遇到过的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class JdkServiceImpl implements JdkService {

@Override
@Metrics(name = "helloJdk")
public void helloJdk() {
// invoke inner
inner();
}

@Metrics(name = "inner")
public void inner() {
System.out.println("this is Jdk inner method");
}
}

当你写了一个自定义注解,然后通过 aop 去拦截时,对于类内部方法之间的调用无法拦截,如上代码片段,在 helloJdk 中调用 inner,同样打上了 @Metrics 注解,实际上 inner 的调用是不会被 aop 拦截到的;原因在于,这里的 inner 调用实际上等同于 this.inner(),而当前的 this 对象是 JdkServiceImpl 对象本身,并非代理类,所以切不到是正常的。

那这里其实就可能通过 AopContext 来辅助一下,这里有个前提条件,通过注解添加配置(加在类上):

1
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)

或通过 xml 配置文件添加配置:

1
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>

否则会抛出如下错误:

Cannot find current proxy: Set ‘exposeProxy’ property on Advised to ‘true’ to make it available.

原因在于,当 exposeProxy 为 true 时,才会将当前 proxy 对象塞到 AopContext 中。

1
2
3
4
5
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

AopContext 的实现很简单,就是在内部维护了一个 ThreadLocal:

1
private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");

所以在使用这个工具类的时候,还需要关注另一个问题,就是当在跨线程,或者使用线程池的情况下,需要手动将 proxy 透传到新的线程中,否则,即使开始 exposeProxy = true,同样也会出现 Cannot find current proxy: Set ‘exposeProxy’ 报错。

SpringAop 代理模式及 AopContext 问题小记

http://www.glmapper.com/2021/07/17/spring/spring-proxy-aopcontext/

作者

卫恒

发布于

2021-07-17

更新于

2022-04-21

许可协议

评论