Marco‘s Java【ByteBudy字节码与Spring AOP共用造成ClassNotFound】
背景
为了避免与Spring AOP技术强绑定,提升组件的可移植性(如将sdk方式转agent方式),提高代理的执行效率等原因,
在Repeat组件中采用ByteBuddy技术对指定类进行代理,但是使用@PreventRepeat组件时发生下述异常
Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class com.corpgovernment.organization.controller.TestController$ByteBuddy$pX8WFoCZ: Common causes of this problem include using a final class or a non-visible class; nested exception is org.springframework.cglib.core.CodeGenerationException: java.lang.NoClassDefFoundError-->com/corpgovernment/organization/controller/TestController$ByteBuddy$pX8WFoCZ] with root cause
java.lang.ClassNotFoundException: com.corpgovernment.organization.controller.TestController$ByteBuddy$pX8WFoCZ
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
...
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:57)
at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:206)
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:482)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:351)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:300)
根据上述异常可以找到一些细节点
1、Class找不到
2、在执行AbstractAutoProxyCreator时找不到
3、底层执行 URLClassLoader 时找不到
这里有两个问题
1、为什么在执行TestController时还要创建Proxy?
2、为什么URLClassLoader找不到Class?
查找问题
为什么在执行TestController时还要创建Proxy?
针对第一个问题,我们可以通过日志推断发现
找不到的Class名称为com.corpgovernment.organization.controller.TestController$ByteBuddy$pX8WFoCZ
我们重新启动项目对AbstractAutoProxyCreator的postProcessAfterInitialization方法进行Debug,发现启动加载的Class是com.corpgovernment.organization.controller.TestController
但由于我们采用ByteBuddy对类com.corpgovernment.organization.controller.TestController进行了代理操作,代理后生成的Class文件名称为com.corpgovernment.organization.controller.TestController$ByteBuddy$pX8WFoCZ
,
而我们执行生成代理操作的时机是InitializingBean,时机晚于postProcessAfterInitialization,所以com.corpgovernment.organization.controller.TestController$ByteBuddy$pX8WFoCZ
没有被Proxy代理
public class ReplaceBeanDefinitionRegistryPostProcessor implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
ByteCodeDynamicEnhancer.enhance("repeat");
...
}
}
这也就解释了为什么后续在执行com.corpgovernment.organization.controller.TestController B y t e B u d d y ByteBuddy ByteBuddypX8WFoCZ类时会进行postProcessAfterInitialization方法创建代理
为什么URLClassLoader找不到Class?
在解释这个问题之前,我们先了解下URLClassLoader是什么?
继承关系:
java.lang.Object
--- java.lang.ClassLoader
--- java.security.SecureClassLoader
--- java.net.URLClassLoader
--- sun.misc.Launcher$ExtClassLoader
java.lang.Object
--- java.lang.ClassLoader
--- java.security.SecureClassLoader
--- java.net.URLClassLoader
--- sun.misc.Launcher$AppClassLoader
根据上述关系可以看出URLClassLoader是sun.misc.Launcher$ExtClassLoader
和sun.misc.Launcher$AppClassLoader
的父类,以下是双亲委派的类加载机制
掌握了上述知识点后,我们对AbstractAutoProxyCreator的createProxy方法进行Debug,查看当前的ClassLoader
可见当前ClassLoader为AppClassLoader
由此推断第二个问题产生的原因是AppClassLoader中加载不到com.corpgovernment.organization.controller.TestController$ByteBuddy$pX8WFoCZ
导致
解决方案
由于篇幅问题,不对ByteBuddy做太多介绍,以下Debug内容为ByteBuddy对com.corpgovernment.organization.controller.TestController类进行代理并生成新的类的代码片段
可以看出被代理生成后的com.corpgovernment.organization.controller.TestController的ClassLoader是ByteArrayClassLoader
根据这个线索,我们可以得出两个解决方案
-
执行AOP时,指定ClassLoader为 ByteArrayClassLoader
-
ByteBuddy进行类加载时,将类注入到 AppClassLoader
考虑到执行AOP时指定ClassLoader可能对其他自定义ClassLoader定制类的操作会产生影响,且Spring无法针对具体的类进行指定ClassLoader操作,所以我们采用第二种方式。
在ByteBuddy注入时可以采用INJECTION的方式将类注入到指定的ClassLoader中,这里我们选择将类注入到AppClassLoader中,问题完美解决!
INJECTION方式本质上是采用反射的方式进行类注入的,而类注入需要访问JVM内部方法,这些方法由安全管理器和Java平台模块系统封闭。
从Java 11开始,除非明确打开这些包,否则无法访问这些方法。