Using the ASM Toolkit for Bytecode Manipulation
Pages: 1, 2, 3, 4
Let's take them one by one, but I should remind you that ASM's visitors can be chained very much the same way as SAX's handlers or filters. This sequence UML diagram shows class transformation, where green classes will be substituted by custom NotifierClassVisitor and NotifierCodeVisitor that will do the
actual bytecode transformation.
The code below uses NotifierClassVisitor to apply all required transformations.
byte[] bytecode;
...
ClassWriter cw = new ClassWriter(true);
NotifierClassVisitor ncv =
new NotifierClassVisitor(cw)
ClassReader cr = new ClassReader(bytecode);
cr.accept(ncv);
Notice the true parameter in the ClassWriter constructor,
which enables the automatic calculation of maximum size of stack and local variables.
In this case, all values passed to the CodeVisitor.visitMax() method
will be ignored and ClassWriter will calculate these values
based on the actual bytecode of the method. However, the CodeVisitor.visitMax()
method still must be called, which happens in its default implementation in
CodeAdapter. This is important because, as you can see in the comparison
results, these values are different for changed bytecode, and with this flag
they will be recalculated automatically, covering item #6 in the list above.
The rest of items will be handled by NotifierClassVisitor.
public class NotifierClassVisitor
extends ClassAdapter implements Constants {
...
The first difference appears in parameters of the visit method,
where the new interface should be added. The code below will cover item #1.
Notice that the cv.visit() method is called to redirect
the transformed processing event to the nested class visitor, which is
actually going to be a ClassWriter object.
We also need to save the class name, since it will be needed later.
public void visit( int version, int access,
String name, String superName,
String[] interfaces, String sourceFile) {
this.className = name;
String[] c;
if( interfaces==null) {
c = new String[ 1];
} else {
int n = 1+interfaces.length;
c = new String[ n];
System.arraycopy(interfaces, 0, c, 0, n);
}
c[ c.length-1] = Notifier.class.getName();
cv.visit( version, access, name, superName,
c, sourceFile);
}
All new elements can be added in the visitEnd()
method just before calling visitEnd() on the chained visitor.
That will cover items #2 and #3 from the list above. Notice that the class name
saved in the visit() method is used instead of a hard-coded constant,
which makes the transformation more generic.
public void visitEnd() {
// adding new field
cv.visitField(ACC_PRIVATE, "__lst",
"Ljava/util/ArrayList;", null, null);
// adding new methods
CodeVisitor cd;
{
cd = cv.visitMethod(ACC_PUBLIC, "notify",
"(Ljava/lang/String;)V", null, null);
cd.visitInsn(ICONST_0);
cd.visitVarInsn(ISTORE, 2);
Label l0 = new Label();
cd.visitLabel(l0);
cd.visitVarInsn(ILOAD, 2);
cd.visitVarInsn(ALOAD, 0);
cd.visitFieldInsn(GETFIELD, className,
"__lst", "Ljava/util/ArrayList;");
...
... see diff above
...
cd.visitInsn(RETURN);
cd.visitMaxs(1, 1);
}
{
cd = cv.visitMethod(ACC_PUBLIC, "addListener",
"(Lasm1/Listener;)V", null, null);
cd.visitVarInsn(ALOAD, 0);
...
... see diff above
...
cd.visitInsn(RETURN);
cd.visitMaxs(1, 1);
}
cv.visitEnd();
}
The rest of the changes belong to method bytecode, so it's necessary
to overwrite the visitMethod() method.
There are two cases have to be covered:
- Add instructions to call
notify()method to all non-static methods. - Add initialization code to all
<init>methods.
In the first case, new instructions are always added to the beginning of the method bytecode,
so chained CodeVisitor can be fired directly.
However, in case of the <init> method, instructions should be added
to the end of method, so they have to be inserted before visitInsn(RETURN),
meaning a custom CodeVisitor is required here. This is how
visitMethod() will look:
public CodeVisitor visitMethod( int access,
String name, String desc,
String[] exceptions, Attribute attrs) {
CodeVisitor cd = cv.visitMethod( access,
name, desc, exceptions, attrs);
if( cd==null) return null;
if( "<init>".equals( name)) {
return new NotifierCodeVisitor( cd, className);
}
if((access & Constants.ACC_STATIC)==0) {
// insert instructions to call notify()
cd.visitVarInsn(ALOAD, 0);
cd.visitLdcInsn(name+desc);
cd.visitMethodInsn(INVOKEVIRTUAL, className,
"notify", "(Ljava/lang/String;)V");
}
return cd;
}
Similar to ClassAdapter, we can extend the CodeAdapter class
and overwrite only those methods that should change the stream of processing events.
In this case, we change the visitInsn() method to
verify if it is an event for the RETURN command and, if so, insert
required commands before delegating the event to the next CodeVisitor
in the chain.
public class NotifierCodeVisitor
extends CodeAdapter {
...
public void visitInsn( int opcode) {
if( opcode==RETURN) {
String type = "java/util/ArrayList";
cv.visitVarInsn(ALOAD,0);
cv.visitTypeInsn(NEW,type);
cv.visitInsn(DUP);
cv.visitMethodInsn(INVOKESPECIAL,
type,"<init>","()V");
cv.visitFieldInsn(PUTFIELD, "asm1/Counter",
"__lst", "L"+type+";");
}
cv.visitInsn(opcode);
}
}
That is basically it. The only piece we have left is the unit test for the whole transformation.