javajavassistlinkageerror

java javassist.CannotCompileException: by java.lang.LinkageError: loader


I need to change the method (calculation formula) without recompiling the application. I know that this can be done with the help of javassist. So far I'm trying on a simple example. In the post method, I call the createMethodHelper () method, which must change the Helper2 method. All OK. But after a repeated call (reloading the page), the error javassist.CannotCompileException: by java.lang.LinkageError: loader So, the class containing the only method I want to change

package ru.testScandJavaCafee.controller;

public class Helper2 {
public String createList()
    {
        System.out.println("++++");
        return "1000";
    }
}

And the class from which I change the method

public String createMethodHelper() throws NotFoundException, CannotCompileException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException, ClassNotFoundException {

        ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(this.getClass()));
        CtClass cc = pool.get("ru.testScandJavaCafee.controller.Helper2");
        cc.defrost();
        CtMethod cm = cc.getMethod("createList","()Ljava/lang/String;" );
        cc.defrost();
        cm.setBody( "{  return \"300 \" ;}" );

        cc.defrost();
        Class c = cc.toClass();
        cc.defrost();
ru.testScandJavaCafee.controller.Helper2 test = (ru.testScandJavaCafee.controller.Helper2) c.newInstance();
        String sum = test.createList();

        return sum;
    }

After a second call (reloading the page), the error

javassist.CannotCompileException: by java.lang.LinkageError: loader (instance of  org/apache/catalina/loader/WebappClassLoader): attempted  duplicate class definition for name: "ru/testScandJavaCafee/controller/Helper2"
    at javassist.ClassPool.toClass(ClassPool.java:1085)
    at javassist.ClassPool.toClass(ClassPool.java:1028)
    at javassist.ClassPool.toClass(ClassPool.java:986)
    at javassist.CtClass.toClass(CtClass.java:1110)
    at ru.testScandJavaCafee.controller.CoffeeTypeController.createMethodHelper(CoffeeTypeController.java:108)
    at ru.testScandJavaCafee.controller.CoffeeTypeController.doPost(CoffeeTypeController.java:56)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:650)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:218)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:506)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:962)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:452)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1087)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.LinkageError: loader (instance of  org/apache/catalina/loader/WebappClassLoader): attempted  duplicate class definition for name: "ru/testScandJavaCafee/controller/Helper2"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at javassist.ClassPool.toClass2(ClassPool.java:1098)
    at javassist.ClassPool.toClass(ClassPool.java:1079)
    ... 27 more

help to understand how to fix the error


Solution

  • I did some research on this from the documentation and this tutorial. And if you want to modify the method multiple times, you can do it this way:

    public static void main(String[] args) {
        try {
            // get a new instance of version 1
            Object newClassInstance1 = m2("com.tima.Helper2", "public String createList() { System.out.println(\"++++\"); return \"200\";}");
            Method method1 = newClassInstance1.getClass().getMethod("createList");
    
            // this executes the createList in the new class com.tima.Helper2
            String sum1 = (String) method1.invoke(newClassInstance1);
            System.out.println("com.tima.Helper2: " + sum1);
    
            // this shows that the original Helper method was not modified
            Helper h = new Helper();
            System.out.println("com.tima.Helper: " + h.createList());
    
            // this shows that the com.tima.Helper2 overrides Helper and can be used as Helper with a modified method
            Helper h2 = (Helper) newClassInstance1;
            System.out.println("com.tima.Helper2 as Helper: " + h2.createList());
    
            // below does the same thing a second time
    
            Object newClassInstance2 = m2("com.tima.Helper3", "public String createList() { System.out.println(\"++++\"); return \"300\";}");
            Method method2 = newClassInstance2.getClass().getMethod("createList");
            String sum2 = (String) method2.invoke(newClassInstance2);
            System.out.println("com.tima.Helper3: " + sum2);
    
            Helper h3 = new Helper();
            System.out.println("com.tima.Helper: " + h3.createList());
    
            Helper h4 = (Helper) newClassInstance2;
            System.out.println("com.tima.Helper3 as Helper: " + h4.createList());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static Object m2(String className, String methodBody) 
            throws CannotCompileException, InstantiationException, IllegalAccessException, NotFoundException {
        // get the pool
        ClassPool classPool = ClassPool.getDefault();
    
        // this seems optional, but if it isn't Main.class (my test class) should be replaced with this.getClass()
        classPool.insertClassPath(new ClassClassPath(Main.class));
    
        // get the helper class
        CtClass helperClass = classPool.get("com.tima.Helper");
    
        // create a new class
        CtClass newCtClass = classPool.makeClass(className);
    
        // make it  child of Helper
        newCtClass.setSuperclass(helperClass);
    
        // this overrides the method in Helper
        newCtClass.addMethod(CtNewMethod.make(methodBody, newCtClass));
    
        // get a new instance
        Class<?> newClass = newCtClass.toClass();
        Object newClassInstance = newClass.newInstance();
    
        return newClassInstance;
    }
    

    Output

    ++++
    com.tima.Helper2: 200
    ++++
    com.tima.Helper: 1000
    ++++
    com.tima.Helper2 as Helper: 200
    ++++
    com.tima.Helper3: 300
    ++++
    com.tima.Helper: 1000
    ++++
    com.tima.Helper3 as Helper: 300
    

    Basically, everytime you call that method it creates a new class, makes the Helper class its superclass and overrides the createList method. The obvious problem here is depending on how many times you call this method you can wind up with a lot of generated classes. So you may want to add checking of if these class are already loaded before creating.

    You say that this method runs when you refresh / load the page so if you just want it to run once, I suggest you put this code in a singleton that runs when your application starts. I'm not sure what you are using for the web application but JSF and Spring both have singleton beans for this type of use.