Who instruments the instrumenters and has a runtime handler?

Around ten months ago I wrote a blog post together with Mikaël Francoeur on how to instrument instrumenters:

Have you ever wondered how libraries like Spring and Mockito modify your code at run-time to implement all their advanced features? Wouldn’t it be cool to get a peek behind the curtains? This is the premise of my meta-agent, a Java agent to instrument instrumenters, to get these insights and what this blog post is about.

Who instruments the instrumenters?

To use the meta-agent you had to attach it manually as an agent:

java -javaagent:target/meta-agent.jar=server -jar your-program.jar

This launched a website under localhost:7071 where you could view the actions of every instrument and transformer. The only problem? It’s cumbersome to use, especially programmatically. Join me in this short blog post to learn about the newest edition of meta-agent and what it can offer.

Instrumentation Handler

An idea that came up at the recent ConFoo conference in discussion with Mikaël and Jonatan Ivanov was to add a new handler mechanism to call code every time a new transformer is added or a class is instrumented. So I got to work.

Every handler has to reside on the of the application and implement the InstrumentationCallback interface

/**
 * A call back called for every instrumentation
 */
public interface InstrumentationCallback {

    /**
     * Called when a new transformer is added
     */
    default CallbackAction onAddTransformer(
      ClassFileTransformer transformer) {
        return CallbackAction.ALLOW;
    }

    /**
     * Called for every existing transformer
     */
    default void onExistingTransformer(
      ClassFileTransformer transformer) {
    }
    
    /**
     * Wraps the transformation, the runnable does the actual transformation
     */
    default byte[] onTransform(ClassFileTransformer transformer, 
      ClassArtifact before, Function<byte[], byte[]> runnable) {
        return runnable.apply(before.bytecode());
    }

    /**
     * Called for every instrumentation
     */
    default CallbackAction onInstrumentation(
      ClassFileTransformer transformer, 
      ClassArtifact before, ClassArtifact after) {
        return CallbackAction.ALLOW;
    }
}

Where a ClassArtifact just combines a class object, the bytecode and a method to decompile the bytecode via vineflower:

public record ClassArtifact(Class<?> klass, byte[] bytecode) {

    public String toJava() {
        return SimpleDecompilation.decompileClass(this);
    }
}

The CallbackAction enum gives us either ALLOW or IGNORE to support not adding a specific transformer or not applying a certain instrumentation.

We can implement a simple handler that just prints the added and existing transformer as follows:

public class LoggingInstrumentationHandler 
  implements InstrumentationCallback {
    @Override
    public CallbackAction onAddTransformer(
      ClassFileTransformer transformer) {
        System.err.println("New transformer " + 
          transformer.getClass().getName());
        return CallbackAction.ALLOW;
    }

    @Override
    public void onExistingTransformer(
      ClassFileTransformer transformer) {
        System.err.println("Existing transformer " + 
          transformer.getClass().getName());
    }
}

To apply this transformer, we pass it as the argument for the cb option to the agent:

java -javaagent:target/meta-agent.jar=cb=LoggingInstrumentationHandler -jar your-program.jar

This initializes the passed handler whenever it is found to be present in a new class loader (albeit it requires that you use a class from that class loader).

In the sample of the meta-agent repository, using the handler looks like

mvn -DargLine="-javaagent:target/meta-agent.jar=server,cb=me.bechberger.meta.LoggingInstrumentationHandler" test

to run it alongside the maven test.

This all has many benefits:

Uses of the Instrumentation Handler

Want to prevent any instrumentation handler from being attached?

public class PreventingInstrumentationHandler 
  implements InstrumentationCallback {
    @Override
    public CallbackAction onAddTransformer(
      ClassFileTransformer transformer) {
        System.err.println("Preventing " + 
          transformer.getClass().getName());
        return CallbackAction.IGNORE;
    }
}

Of course, this only works if the meta-agent is the first attached agent.

Want to crash the application if any non-mockito-related instrumentation is performed?

public class PreventNonMockitoInstrumentationHandler 
  implements InstrumentationCallback {
    @Override
    public CallbackAction onAddTransformer(
      ClassFileTransformer transformer) {
        commonOnAddTransformer(transformer);
        return CallbackAction.ALLOW;
    }

    @Override
    public void onExistingTransformer(
      ClassFileTransformer transformer) {
        commonOnAddTransformer(transformer);
    }
    
    void commonOnAddTransformer(ClassFileTransformer transformer) {
        if (!transformer.getClass().getName()
                        .startsWith("org.mockito")) {
            System.err.println("Unsupported transformer added " + 
              transformer.getClass().getName());
            System.exit(1);
        }
    }
}

This crashes for every transformer whose class doesn’t belong to a Mockito package. This can be helpful in CI or highly managed environments when you want to make sure that only specific transformers are used.

Want to test that a transformer really works? You can write your own test helper instrumentation handler that captures transformations with onInstrumentation.

Want to emit modified bytecode and decompiled Java code for each instrumentation? Just emit the files in onInstrumentation method.

Want to modify the bytecode before or after the transformation by a specific transformer? The more advanced onTransform(ClassFileTransformer transformer,
ClassArtifact before, Function runnable)
method has you covered.

There a probably countless more use cases for this API, feel free to point them out or file bug and feature requests in the meta-agent repository.

There is only one problem with the meta-agent: It’s still cumbersome to use because you have to set the -DargLine option for every test.

Maven Plugin

This is why I implemented my first Maven plugin (it’s not hard). This plugin adds the meta-agent automatically to the test command line. To use it, just add the following to your pom:

<build>
    <plugins>
        <plugin>
            <groupId>me.bechberger</groupId>
            <artifactId>meta-agent-maven-plugin</artifactId>
            <version>0.0.3</version>
            <executions>
                <execution>
                    <phase>validate</phase>
                    <goals>
                        <goal>meta-agent</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <callbackClasses> <!-- to add an instrumentation callback handler -->
                    <callbackClass>me.bechberger.meta.LoggingInstrumentationHandler</callbackClass>
                </callbackClasses>
                <server>true</server> <!-- to start the meta-agent server -->
            </configuration>
        </plugin>
    </plugins>
</build>

And the following test dependency:

<dependency>
    <groupId>me.bechberger</groupId>
    <artifactId>meta-agent</artifactId>
    <version>0.0.3</version>
    <scope>test</scope>
</dependency>

This allows you to directly configure your handlers in your pom and you can use it to enable or disable the meta-agent server.

Conclusion

In this blog post, you saw how I extended the instrumenting agent for instrumenting instrumenting agents to be actually useful for more than just debugging. This opens doors for many CI and testing use cases. Adding an additional maven plugin makes this even easier and I was surprised how easy implementing the plugin was.

Thanks for coming this far, if you want to see a talk based on the meta-agent, come to VoxxedDays Amsterdam in early April.

This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone.

Quebec City in Winter

Author

  • Johannes Bechberger

    Johannes Bechberger is a JVM developer working on profilers and their underlying technology in the SapMachine team at SAP. This includes improvements to async-profiler and its ecosystem, a website to view the different JFR event types, and improvements to the FirefoxProfiler, making it usable in the Java world. He started at SAP in 2022 after two years of research studies at the KIT in the field of Java security analyses. His work today is comprised of many open-source contributions and his blog, where he writes regularly on in-depth profiling and debugging topics, and of working on his JEP Candidate 435 to add a new profiling API to the OpenJDK.

    View all posts

New posts like these come out at least every two weeks, to get notified about new posts, follow me on BlueSky, Twitter, Mastodon, or LinkedIn, or join the newsletter:

Leave a Reply

Your email address will not be published. Required fields are marked *