Mesbah Gueffaf
Mesbah Gueffaf

Reputation: 558

Changing a class with javassist (java reflexion)

I have the following code. I want to change the say method of the hello class. I use javassist. I have the following error.

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();
}

}

The hello class :

public class Hello {

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

The error message:

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"

Upvotes: 9

Views: 6314

Answers (4)

dong sheng
dong sheng

Reputation: 364

 //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 .

Upvotes: 0

SkyWalker
SkyWalker

Reputation: 29150

Code:

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();
    }

}

Debug and solution:

If you remove the following 2 lines, then it will run successfully.

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

Output:

Hello.say():

Hello

Root Cause Analysis:

When first time you use Hello h1 = new Hello();, the classloader loads the Hello class.

After that when you again try to load the Hello class by using cc.toClass();, this error comes.

Reason of occurrence:

Rafael Winterhalter told the reason and some way of solutions in this link as

cc.toClass() takes a loaded class[Hello] and redefines this very same class without changing its name. After this redefinition, you attempt to load the altered class one more time. This is however not possible in Java where any ClassLoader can only load a class of a given name one single time.

To overcome your problem, you have different choices:

  1. Create a subclass of the argument class (or use interfaces) which uses a random name. The subclass is then type compatible to your argument class but is never loaded.
  2. Use the Instrumentation API to redefine your loaded class at runtime.
  3. Make sure that the input class and the output class are loaded with different ClassLoaders. (not recommended)

Same type of issues are described here:

In tomcat, they have solved the issue.

  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

Upvotes: 11

rvit34
rvit34

Reputation: 2116

You should load both origin and modified versions of class in the different class loaders. Try this:

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();
    }
}   

If your goal is to modify already loaded class at the same classloader you might use Java Instrument API to retransform class after javaasist worked

Upvotes: 0

Naveen Ramawat
Naveen Ramawat

Reputation: 1445

Don't know but somehow "Hello h1 = new Hello();" line creating issue. See the below updated code which is working.

    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();

Upvotes: -1

Related Questions