ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

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.

Pages: 1, 2, 3, 4

Next Pagearrow