bytekit
bytekit copied to clipboard
使用Bytekit 做Agent 对所有类做增强时,会出现`duplicate class definition`的错误
环境: Windows 10 64 bit/Oracle JDK8
问题复现的demo(必现问题) -> https://github.com/kongwu-/bytekit-agent-sample
Transform类很简单,什么增强内容都没做,只是调用了一下AsmUtils.toClassNode
public class SmpClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("enhance... "+className);
try {
ClassNode classNode = AsmUtils.toClassNode(classfileBuffer);
// 获取增强后的字节码
// 打印ClassLoader 日志之后,发现这里会进行Class load,而且ClassLoader是Launcher$AppClassLoader
// 这个toBytes 会 load class 有点不太理解,只是生成字节码需要 define 一次嘛……
byte[] bytes = AsmUtils.toBytes(classNode);
//transformer 返回后,会再次打印一遍Class Load日志,ClassLoader 仍然是 Launcher$AppClassLoader
return bytes;
}catch (Error e){
e.printStackTrace();
return classfileBuffer;
}
}
最后打包 Agent 运行时,就会出现重复定义类的错误:loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name: "org/springframework/core/NestedRuntimeException"
(理论上很多类都会出现这个问题,只是碰巧报错的是Spring的)。
transform 方法中load 了两遍,也没报错,这里应该算是第三遍load了,此时发生错误:duplicate class definition
stack trace 也有一点奇怪:
Exception in thread "main" java.lang.LinkageError: loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name: "org/springframework/core/NestedRuntimeException"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
//native define Class 之后,又调用 loadClass
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at org.example.App.main(App.java:13)
按道理说,同一个ClassLoader,define一次之后就被缓存了,再次loadClass时就会从缓存中获取了,这里报错实在想不通。
原因如下,在 agent 加载 org/springframework/core/NestedRuntimeException 的过程当中,在这行 AsmUtils.toBytes(classNode) 会 load 一次,参见代码 ClassLoaderAwareClassWriter.class:
@Override
protected String getCommonSuperClass(String type1, String type2) {
if (classLoader == null) {
return super.getCommonSuperClass(type1, type2);
}
Class<?> c, d;
try {
c = Class.forName(type1.replace('/', '.'), false, classLoader);
d = Class.forName(type2.replace('/', '.'), false, classLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (c.isAssignableFrom(d)) {
return type1;
}
if (d.isAssignableFrom(c)) {
return type2;
}
if (c.isInterface() || d.isInterface()) {
return "java/lang/Object";
} else {
do {
c = c.getSuperclass();
} while (!c.isAssignableFrom(d));
return c.getName().replace('.', '/');
}
}
在上述代码 load 一次之后,由于 agent 拦截代码运行在 defineClass1 这个 native 方法里面 所以会导致在 defineClass1 执行期间重复 define 抛出异常
解决方案的话,可以传入自定义 classloader,但是可能又会引入 classloader 加载路径问题
同时在执行期间,发现 org/springframework/core/NestedRuntimeException 这个类加载时, getCommonSuperClass 里面的 type1 type2 分别是 Object 和 NestedRuntimeException ,很明显 NestedRuntimeException 是无效参数,所以重写这个类,加个条件判断可以临时解决。
临时修复后的方法
private final String self;
@Override
protected String getCommonSuperClass(String type1, String type2) {
if (classLoader == null) {
return super.getCommonSuperClass(type1, type2);
}
//https://github.com/alibaba/bytekit/issues/7
if (type1.equals(self)) {
return type2;
}
if (type2.equals(self)) {
return type1;
}
Class<?> c, d;
try {
c = Class.forName(type1.replace('/', '.'), false, classLoader);
d = Class.forName(type2.replace('/', '.'), false, classLoader);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (c.isAssignableFrom(d)) {
return type1;
}
if (d.isAssignableFrom(c)) {
return type2;
}
if (c.isInterface() || d.isInterface()) {
return "java/lang/Object";
} else {
do {
c = c.getSuperclass();
} while (!c.isAssignableFrom(d));
return c.getName().replace('.', '/');
}
}
遇到同样的问题,参考:https://stackoverflow.com/questions/69563714/linkageerror-attempted-duplicate-class-definition-when-dynamically-instrument