Изменение класса с помощью javassist (рефлексия Java)

У меня есть следующий код. Я хочу изменить метод say класса hello. Я использую джавасист. У меня следующая ошибка.

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"

person Mesbah Gueffaf    schedule 18.10.2016    source источник
comment
Взгляните на byte buddy, он намного мощнее и проще в использовании.   -  person Ashutosh Jha    schedule 18.10.2016


Ответы (3)


Код:

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

}

Отладка и решение:

Если вы удалите следующие 2 строки, он будет успешно запущен.

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

Вывод:

Привет.скажи():

Привет

Анализ причин:

Когда вы впервые используете Hello h1 = new Hello();, загрузчик классов загружает класс Hello.

После этого, когда вы снова пытаетесь загрузить класс Hello с помощью cc.toClass();, возникает эта ошибка.

Причина возникновения:

Рафаэль Винтерхальтер рассказал причину и пути решения в этом ссылка как

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
person SkyWalker    schedule 25.10.2016
comment
Спасибо. Для приведенного выше примера это работает. Но если мы захотим добавить новый метод, он не сработает. Согласно документу API. Переопределение не должно добавлять, удалять или переименовывать поля или методы. Итак, как мы можем добавить методы. Ссылка: docs.oracle.com/javase/6/docs/api/java/lang/instrument/ - person Nitul; 14.02.2018

Вы должны загружать как исходные, так и модифицированные версии класса в разные загрузчики классов. Попробуй это:

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

Если ваша цель — изменить уже загруженный класс в том же загрузчике классов, вы можете использовать API инструментов Java в повторно преобразовать класс после работы javaasist

person rvit34    schedule 25.10.2016

Не знаю, но как-то "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();
person Naveen Ramawat    schedule 18.10.2016
comment
мы намеренно назвали Hello h1 = new Hello(). Потому что мы хотим изменить класс, экземпляр которого уже создан. - person Mesbah Gueffaf; 18.10.2016