I have code that attempts to redefine a class at runtime using a ClassFileTransformer
and an instance of Instrumentation
.
However, I've noticed that the transform
method of ClassFileTransformer
fails to throw any exceptions.
Here's an example (tested in java 8 and 17):
import net.bytebuddy.agent.ByteBuddyAgent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class Main {
public static void main(String[] args) {
// implementation 'net.bytebuddy:byte-buddy-agent:1.14.13'
ByteBuddyAgent.install();
Instrumentation instrumentation = ByteBuddyAgent.getInstrumentation();
instrumentation.addTransformer(new Transformer(), true);
try {
instrumentation.retransformClasses(Klass.class);
}catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
return;
}
System.out.println("Finished successfully");
}
private static class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader cl, String name, Class<?> klass, ProtectionDomain pd, byte[] classfileBuffer) {
System.out.println("Transforming class " + klass.getName());
throw new RuntimeException("Example exception");
}
}
private static class Klass {}
}
This code throws an exception and should logically trigger the "An error occurred: " message.
Instead, no exception is caught by the catch block:
Transforming class Main$Klass
Finished successfully
Do you know what causes this behavior and whether it can be avoided?
User Slaw was right when commenting:
From
ClassFileTransformer
: "If the transformer throws an exception (which it doesn't catch), subsequent transformers will still be called and the load, redefine or retransform will still be attempted. Thus, throwing an exception has the same effect as returning null."
The credit for this hint is his/hers. I want to add some details, though:
Just print the stack trace (or debug into it) to see how your transform
method is called:
private static class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader cl, String name, Class<?> klass, ProtectionDomain pd, byte[] classfileBuffer) {
System.out.println("Transforming class " + klass.getName());
new RuntimeException("Example exception").printStackTrace(System.out);
throw new RuntimeException("Example exception");
}
}
Console log for JDK 21:
Transforming class Main$Klass
java.lang.RuntimeException: Example exception
at Main$Transformer.transform(Main.java:27)
at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:610)
at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:225)
at Main.main(Main.java:14)
Finished successfully
Now, let us take a look at JDK class TransformerManager
to find the place in the code where your exception is intentionally swallowed (whitespace slightly reformatted):
try {
transformedBytes = transformer.transform(module, loader, classname, classBeingRedefined, protectionDomain, bufferToUse);
}
catch (Throwable t) {
// don't let any one transformer mess it up for the others.
// This is where we need to put some logging. What should go here? FIXME
}
I.e., throwing an IllegalClassFormatException
instead of the generic RuntimeException
, as suggested by the javadoc, does not log anything either, at least not up until JDK 22.