使用Javassist(Java反射)更改类

9
我有以下代码。我想要改变hello类的say方法。我使用javassist。我遇到了以下错误。
public class TestJavasisit {
/**
 * @param args the command line arguments
 * @throws java.lang.Exception
 */
public static void main(String[] args) throws Exception {
    ClassPool pool = ClassPool.getDefault();
    // version original
    Hello h1 = new Hello();
    h1.say();
    CtClass cc = pool.get("testjavasisit.Hello");
    cc.defrost();
    CtMethod m = cc.getDeclaredMethod("say");
    m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
    cc.writeFile(".");
    cc.toClass();
    // version modifie
    Hello h2 = new Hello();
    h2.say();
}

}

你好类:

public class Hello {

    public void say() {
        System.out.println("Hello");
    }
}

错误信息:
run:
Hello
Exception in thread "main" javassist.CannotCompileException: by java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "testjavasisit/Hello"

2
看一下Byte Buddy,它更强大且易于使用。 - Ashutosh Jha
4个回答

11

代码:

package testjavasisit;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class TestJavasisit {
    /**
     * @param args
     *            the command line arguments
     * @throws java.lang.Exception
     */
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        // version original
         Hello h1 = new Hello(); // remove this line
         h1.say();               // remove this line

        CtClass cc = pool.get("testjavasisit.Hello");
        cc.defrost();
        CtMethod m = cc.getDeclaredMethod("say");
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
        cc.writeFile(".");
        // This throws a java.lang.LinkageError ... attempted  duplicate class definition for name: ...
        cc.toClass();
        // version modifie
        Hello h2 = new Hello();
        h2.say();
    }

}

调试和解决方案:

如果删除以下两行代码,则程序将成功运行。

   // Hello h1 = new Hello();
   // h1.say();

输出:

Hello.say():

Hello

根本原因分析:

当您第一次使用Hello h1 = new Hello();时,类加载器会加载Hello类。

之后,当您再次尝试使用cc.toClass();加载Hello类时,就会出现此错误。

发生的原因:

Rafael Winterhalter在这个链接中给出了发生的原因和一些解决方法。

cc.toClass() 接收一个已经被加载的类 [Hello] 并且重新定义了这个同名的类,但不改变它的名字。在这个重新定义之后,您尝试再次加载这个被更改过的类。然而,在Java中这是不可能的,因为任何 ClassLoader 只能 载入一个给定名称的类一次

要解决您的问题,有几种选择:

  1. 创建一个参数类的子类(或使用接口),并使用随机名称。这个子类具有与参数类兼容的类型,但永远不会被加载。
  2. 在运行时使用Instrumentation API 重新定义您已加载的类。
  3. 确保输入类和输出类是由不同的类加载器加载的。(不建议)

这里描述了相同类型的问题:

在Tomcat中,他们已经解决了这个问题。

  1. https://github.com/brettwooldridge/HikariCP/issues/217
  2. http://forum.spring.io/forum/spring-projects/container/58952-use-javassist-modify-class-error-when-in-lib

谢谢。上面的例子是有效的。但如果我们想添加新方法,它就不起作用了。根据API文档,重新定义不得添加、删除或重命名字段或方法。那么我们该如何添加方法呢?链接:https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/Instrumentation.html#redefineClasses(java.lang.instrument.ClassDefinition...) - Nitul

0

你应该在不同的类加载器中加载原始版本和修改后的类。 试试这个:

public static void main(String[] args) {
     try {
         ClassPool pool = ClassPool.getDefault();
         Loader cl = new Loader(pool); //javassist.Loader
         // version original
         Class origin = cl.loadClass("testjavasisit.Hello");
         Object h1 =  origin.newInstance();
         Method sayMethod =  origin.getMethod("say", null);
         sayMethod.invoke(h1);

            CtClass cc = pool.get("testjavasisit.Hello");
            cc.defrost();
            CtMethod m = cc.getDeclaredMethod("say");
            m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
            cc.writeFile(".");
            cc.toClass();
            // version modifie            
            Hello h2 = new Hello();
            h2.say();
    } catch (Throwable e) {
        e.printStackTrace();
    }
}   

如果您的目标是在同一类加载器上修改已加载的类,您可以使用Java Instrument API来在javaasist工作后重新转换类。

0
 //0.just use javaassist ,gradle dependency :
 //compile 'org.javassist:javassist:3.21.0-GA'
 //1.try to find the class  
 ctClazz.refClasses.forEach {
        if (it == "android.widget.Toast") {
            //yes, you find the target class
        }
    }
 ctClazz.replaceClassName("com.xx.the.class","com.xx.the.target.class")
 ctClazz.writeFile(classDirectory)
 ctClazz.xxx() //if need

 //2try to recompile the jar and check out .

-1

不知道为什么 "Hello h1 = new Hello();" 这一行会出问题。请看下面更新后的代码,它可以正常工作。

    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.get("testjavasisit.Hello");
    cc.defrost();
    CtMethod m = cc.getDeclaredMethod("say");
    m.insertBefore("{ System.out.println(\"Hello11.say():\"); }");
    cc.writeFile("build");
    cc.toClass();
    Hello h2 = new Hello();
    h2.say();

我们故意称呼Hello h1 = new Hello()。因为我们想要更改已经实例化的类。 - Mesbah Gueffaf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接