bytekit icon indicating copy to clipboard operation
bytekit copied to clipboard

使用Bytekit 做Agent 对所有类做增强时,会出现`duplicate class definition`的错误

Open kongwu- opened this issue 3 years ago • 2 comments

环境: 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时就会从缓存中获取了,这里报错实在想不通。

kongwu- avatar May 09 '21 14:05 kongwu-

原因如下,在 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('.', '/');
        }
    }

chenweixuanJokes avatar Dec 01 '21 01:12 chenweixuanJokes

遇到同样的问题,参考:https://stackoverflow.com/questions/69563714/linkageerror-attempted-duplicate-class-definition-when-dynamically-instrument

the-wang avatar May 28 '22 01:05 the-wang