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.
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:
Transforms the Greeter#greet()
method to print "Goodbye, World!"
instead of "Hello, World!"
.
Uses the Java 22 Class-File API preview feature (see java.lang.classfile
) to transform the method body to match what you're doing in your question. Note using that API is not a requirement; you can use any other byte-code manipulation library to perform the transformation. Technically, you could even implement your own code/library for this, though I wouldn't recommend doing so.
Registers the agent with Launcher-Agent-Class
, which is specific to executable JARs. See the java.lang.instrument
package documentation for alternatives.
Has the agent designed in a way that allows transforming the Greeter
class at an arbitrary point in time (rather than immediately when the agentmain
method is invoked).
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()
).
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.MF
Main-Class: com.example.Main
Launcher-Agent-Class: com.example.ExampleAgent
Can-Retransform-Classes: true
<PROJECT-DIR>
|
\---src
+---com
| \---example
| ExampleAgent.java
| Greeter.java
| Main.java
|
\---META-INF
MANIFEST.MF
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
Hello, World!
Goodbye, World