javainstrumentationjava-bytecode-asmbyte-buddy

Java ClassFileTransformer fails to throw exception


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?


Solution

  • 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.