An introduction to Java Agent and bytecode manipulation
📅 Published: • Thomas Queste
A few months ago, I wrote a little shell script to colorize Maven’s output. This is a good solution, but Jean-Christophe Gay has written a pure Java version to solve the same problem with interesting bits of Java technologies: Java Agent and bytecode manipulation using ASM.
In this post, we will see how a Java Agent and some bytecode manipulation can open the inner guts of code we don’t own.
Code sample
I wrote a little test project to wire the stuff involved. There are two Maven modules, one for the Agent and one for representing the external lib (we should not modify it).
The whole code is on GitHub
Agent
First, we need a Java Agent to inject and intercept real code. The agent framework is part of the JDK and allows us to operate on classes before (and even after) they are used.
Here is the code of the Agent. Note the premain static method which is called at start.
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new Transformer() {
@Override
public byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
// Intercept the call to the class Stuff
if ("other/Stuff".equals(s)) {
// ASM Code
ClassReader reader = new ClassReader(bytes);
ClassWriter writer = new ClassWriter(reader, 0);
ClassPrinter visitor = new ClassPrinter(writer);
reader.accept(visitor, 0);
return writer.toByteArray();
}
return null;
}
});
}
}
The agent is called because the JVM is launched with the -javaagent:myjar.jar
parameter and because the Manifest indicates this class.
Command line :
$ java -javaagent:agent/target/agent-0.1-SNAPSHOT.jar -jar other/target/other-0.1-SNAPSHOT.jar
ASM
To manipulate bytecode, I used the ASM framework.
ASM is a Java bytecode manipulation and analysis framework used in many products.
In the example above, the ClassPrinter prints the signatures of the classes and methods visited.
public class ClassPrinter extends ClassVisitor {
public ClassPrinter(ClassWriter writer) {
super(Opcodes.ASM4, writer);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(" " + name + desc);
return super.visitMethod(access, name, desc, signature, exceptions);
}
@Override
public void visitEnd() {
System.out.println("}");
super.visitEnd();
}
}
Here is the output, as we intercepted the call to the Stuff
class :
other/Stuff extends java/lang/Object {
<init>()V
run()V
}
I AM DOING SOME STUFF...
Javassist
Javassist is another bytecode manipulation framework. I found it simpler due to higher level methods like addLocalVariable
, insertBefore
, insertAfter
…
Here is a version of the Agent used to output the elapsed time for executing the run
method :
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
if ("other/Stuff".equals(s)) {
// Javassist
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("other.Stuff");
CtMethod m = cc.getDeclaredMethod("run");
m.addLocalVariable("elapsedTime", CtClass.longType);
m.insertBefore("elapsedTime = System.currentTimeMillis();");
m.insertAfter("{elapsedTime = System.currentTimeMillis() - elapsedTime;"
+ "System.out.println(\"Method Executed in ms: \" + elapsedTime);}");
byte[] byteCode = cc.toBytecode();
cc.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return null;
}
});
}
}
Output :
I AM DOING SOME STUFF...
Method Executed in ms: 1
Real case : Maven colorization
Jean-Christophe Gay hacked Maven to colorize the output and to highlight warnings and errors. And it did that without altering Maven, but from the outside. The idea is to start Maven with an agent which enhances Maven (3.0) output with colorization.
It is interesting as a real world scenario. Maven does not expose an API to access the log in real time, and also because one needs to know exactly where to inject the custom code and how fragile it is to hack a third-party piece of code.
You can see his code here : https://github.com/jcgay/maven-color
In its code, the author had to :
- start the agent when the target program is run, this was done using the
MAVEN_OPTS
environment variable - find where Maven was outputting the relevant logs message (well, this has to be done for Maven and Surefire, which is even more susceptible to change.
For example, this is how the new method for output colorization is created with pure bytecode injection, using ASM :
private void createSetupAnsiColorLoggerMethod() {
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PRIVATE, "setupLogger", "(Lorg/apache/maven/cli/MavenCli$CliRequest;)Lorg/codehaus/plexus/logging/Logger;", null, null);
mv.visitCode();
mv.visitTypeInsn(Opcodes.NEW, "com/github/jcgay/maven/color/logger/AnsiColorLogger");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/github/jcgay/maven/color/logger/AnsiColorLogger", "<init>", "()V");
mv.visitVarInsn(Opcodes.ASTORE, 2);
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitFieldInsn(Opcodes.GETFIELD, "org/apache/maven/cli/MavenCli$CliRequest", "request", "Lorg/apache/maven/execution/MavenExecutionRequest;");
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "org/apache/maven/execution/MavenExecutionRequest", "getLoggingLevel", "()I");
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "org/codehaus/plexus/logging/Logger", "setThreshold", "(I)V");
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(2, 3);
mv.visitEnd();
}
Reading Bytecode for Dummies will help, for sure.
For another real world usage, there is Byteman written by JBoss. This tool simplifies testing of Java programs. The documentation is difficult to read, IMHO, but the presentation I saw three years ago was really stunning. Byteman can be used to make untestable code testable.