I started learning Java Agent few days ago. But documentation is not very good and beginners like me struggling to understand the basics. I created a basic multiplier class and export it to runnable jar using eclipse. Here is the code snippet.
Main jar file:
public class Multiplier {
public static void main(String[] args) {
int x = 10;
int y = 25;
int z = x * y;
System.out.println("Multiply of x*y = " + z);
}
}
Now I want to manipulate the value of x from an agent. I tried to create the Agent class like this
Agent:
package myagent;
import org.objectweb.asm.*;
import java.lang.instrument.*;
public class Agent {
public static void premain(final String agentArg, final Instrumentation inst) {
System.out.println("Agent Started");
int x_modified = 5;
//Now How to push the new value (x_modified) to the multiplier class?
//I know I have to use ASM but can't figure it out how to do it.
//Result should be 125
}
}
My Question
How do I set the value of x from agent class to multiplier class using ASM? Result should be 125.
The first thing, your agent has to do, is registering a ClassFileTransformer
. The first thing, the class file transformer should do in its transform
method, is checking the arguments to find out whether the current request is about the class we’re interested in, to return immediately if not.
If we are at the class we want to transform, we have to process the incoming class file bytes to return a new byte array. You can use ASM’s ClassReader
to process to incoming bytes and chain it to a ClassWriter
to produce a new array:
import java.lang.instrument.*;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
public class ExampleAgent implements ClassFileTransformer {
private static final String TRANSFORM_CLASS = "Multiplier";
private static final String TRANSFORM_METHOD_NAME = "main";
private static final String TRANSFORM_METHOD_DESC = "([Ljava/lang/String;)V";
public static void premain(String arg, Instrumentation instrumentation) {
instrumentation.addTransformer(new ExampleAgent());
}
public byte[] transform(ClassLoader loader, String className, Class<?> cl,
ProtectionDomain pd, byte[] classfileBuffer) {
if(!TRANSFORM_CLASS.equals(className)) return null;
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(
access, name, desc, signature, exceptions);
if(name.equals(TRANSFORM_METHOD_NAME)
&& desc.equals(TRANSFORM_METHOD_DESC)) {
return new MethodVisitor(Opcodes.ASM5, mv) {
@Override
public void visitIntInsn(int opcode, int operand) {
if(opcode == Opcodes.BIPUSH && operand == 10) operand = 5;
super.visitIntInsn(opcode, operand);
}
};
}
return mv;
}
}, 0);
return cw.toByteArray();
}
}
Note that by passing the ClassWriter
to our custom ClassVisitor
’s constructor and passing the MethodVisitor
returned by the super.visitMethod
invocation to our MethodVisitor
’s constructor, we enable a chaining that reproduces the original class by default; all methods we’re not overriding will delegate to the specified ClassWriter
/MethodVisitor
reproducing the encountered artifact. Compare with the tutorial about ASM’s event model.
The example above enables an optimization by also passing the ClassReader
instance to the ClassWriter
’s constructor. This improves the efficiency of instrumenting a class when only making small changes, like we do here.
The crucial part is overriding visitMethod
to return our custom MethodVisitor
when we are at the “hot” method and overriding visitIntInsn
to change the desired instruction. Note how these methods delegate to the super
calls when not altering the behavior, just like the methods we didn’t override.