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,
method has you covered.
ClassArtifact before, Function runnable)
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.
