javaclassjvmclassloader

How can I reload a modified class at runtime in Java?


I am writing my bachelor thesis about libraries for runtime code manipulation in Java. I have to work on a profiler for the practical part that can inject code into loaded classes which are marked with an annotation. I am working with the new Java ClassFile API and everything works as planned up to the point where I want my changes to take effect.

I have transformed the method bodies of some methods in a class (to include timers, etc., to measure how long a function takes, etc.) and now I have a byte array containing the modified class. The issue now is that I can't figure out how to load this new class and make its changes affect all following instances of the modified classes.

Test before = new Test();
before.doNothing();

Profiler profiler = Profiler.getInstance();
profiler.inject();

Test after = new Test();
after.doNothing();

This is the test code. The Test class has a function that does nothing literally, the bytecode is just a return. The profiler injects bytecode of a print statement that prints 20. But it's not being affected by any changes. I tried loading it with a custom class loader, and I was able to create a new instance of that custom loaded class and it printed 20, but that did not override the actual Test class, but create an additional one, and the doNothing of the actual Test class was still the same, doing nothing.

So by that, I know that the modification itself works, it's just not properly applied. I need a way to update the class globally somehow with the new class I have in a byte array (I can also write it in a temp file if that helps), and it should just happen at runtime, no actual overwriting or compile-time stuff.


Solution

  • If all you want to do is transform the body of a method, then you can do that via instrumentation. Here is an example which:

    Note the retransformation affects instances of the class created before the retransformation. Also note the implementation of ExampleAgent is only good enough for the example (for instance, it doesn't check if you've already invoked #retransformGreeterClass()).

    Source Code

    Greeter.java

    package com.example;
    
    public class Greeter {
    
      public void greet() {
        System.out.println("Hello, World!");
      }
    }
    

    Main.java

    package com.example;
    
    public final class Main {
    
      public static void main(String[] args) {
        var greeter = new Greeter();
    
        greeter.greet();
        ExampleAgent.retransformGreeterClass();
        greeter.greet();
      }
    }
    

    ExampleAgent.java

    package com.example;
    
    import static java.lang.classfile.ClassTransform.transformingMethodBodies;
    
    import java.lang.classfile.ClassFile;
    import java.lang.classfile.CodeBuilder;
    import java.lang.classfile.CodeElement;
    import java.lang.classfile.Instruction;
    import java.lang.classfile.MethodModel;
    import java.lang.classfile.Opcode;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.UnmodifiableClassException;
    import java.security.ProtectionDomain;
    
    public class ExampleAgent {
    
      private static Instrumentation inst;
    
      public static void agentmain(String agentArgs, Instrumentation inst) {
        ExampleAgent.inst = inst;
      }
    
      public static void retransformGreeterClass() {
        inst.addTransformer(new GreeterClassFileTransformer(), true);
        try {
          inst.retransformClasses(Greeter.class);
        } catch (UnmodifiableClassException ex) {
          throw new RuntimeException(ex);
        }
      }
    
      private static class GreeterClassFileTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(
            ClassLoader loader,
            String className,
            Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain,
            byte[] classfileBuffer) {
          if ("com/example/Greeter".equals(className)) {
            var cf = ClassFile.of();
            return cf.transform(
                cf.parse(classfileBuffer),
                transformingMethodBodies(this::isGreetMethod, this::acceptGreetMethodElement));
          }
          return null;
        }
    
        private boolean isGreetMethod(MethodModel model) {
          return model.methodName().equalsString("greet");
        }
    
        private void acceptGreetMethodElement(CodeBuilder builder, CodeElement element) {
          if (element instanceof Instruction i && i.opcode() == Opcode.LDC) {
            builder.ldc("Goodbye, World!");
          } else {
            builder.with(element);
          }
        }
      }
    }
    

    Manifest

    MANIFEST.MF

    Main-Class: com.example.Main
    Launcher-Agent-Class: com.example.ExampleAgent
    Can-Retransform-Classes: true
    

    Project Directory

    <PROJECT-DIR>
    |
    \---src
        +---com
        |   \---example
        |           ExampleAgent.java  
        |           Greeter.java       
        |           Main.java
        |
        \---META-INF
                MANIFEST.MF
    

    Building & Executing

    Working directory is <PROJECT-DIR>.

    Compiling:

    javac --enable-preview --release 22 --source-path src -d out/classes src/com/example/*.java
    

    Packaging:

    jar cfm out/example.jar src/META-INF/MANIFEST.MF -C out/classes .
    

    Executing:

    java --enable-preview -jar out/example.jar
    

    Output

    Hello, World!
    Goodbye, World