天天看點

ASM技術研究ASM技術研究

ASM技術研究

ASM是什麼

ASM 是一個 Java 位元組碼操控架構。它能被用來動态生成類或者增強既有類的功能。ASM 可以直接産生二進制 class 檔案,也可以在類被加載入 Java 虛拟機之前動态改變類行為。ASM 提供類似于 BCEL 和 SERP 之類的工具包的功能,但是被設計得更小巧、更快速,這使它适用于實時代碼插裝。

ASM原理

Java class 被存儲在嚴格格式定義的 .class 檔案裡,這些類檔案擁有足夠的中繼資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM 就是從類檔案中讀入這些資訊,并且提供了通路和修改這些資訊的接口,進而改變類的行為。對于 ASM 來說,Java class 被描述為一棵樹,ASM使用 “Visitor”(通路者) 模式周遊整個二進制結構,以事件驅動的處理方式将對類資訊的操作接口傳遞給使用者(通路者),使用者隻需要關注于對其業務有意義的部分,而不必了解 Java 類檔案格式的所有細節。

ASM使用方式

如果對類的修改是一次性的且原始類資訊是可知的,那麼可以使用ASM直接編譯出修改過的class并将其儲存到硬碟,之後運作期不再依賴ASM,和普通的類沒有差別,這種方式通常需要自定義ClassLoader,如ConverClassLoader等。

如果不希望改變原有的類檔案,隻是在運作期添加或修改一些類資訊的話,比如動态代理、AOP等,可以使用java的Instrument技術,啟動時往 Java 虛拟機中挂上一個使用者定義的 hook 程式,在裝入特定類的時候使用ASM改變特定類的位元組碼,進而改變該類的行為,舉例如下:

public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new ClassFileTransformer() {
    public byte[] transform(ClassLoader l, String name, Class c,
    ProtectionDomain d, byte[] b)
    throws IllegalClassFormatException {
    ClassReader cr = new ClassReader(b);
    ClassWriter cw = new ClassWriter(cr, );
    ClassVisitor cv = new ChangeVersionAdapter(cw);
    cr.accept(cv, );
    return cw.toByteArray();
    }
    });
}
           

另外,當然也可以在程式的任何位置使用ASM,這些都是沒有限制的,ASM從本質上說也是一個java類庫,當然可以随時使用其功能。

類資訊通路次序

ASM是以事件驅動的處理方式将類資訊傳遞給使用者,使用者要接收這些資訊需要實作特定的Visitor(通路者),Visitor中的方法是由ASM依據對類資訊的周遊順序進而調用的,ASM中定義了多種Visitor,以ClassVisitor為例,方法調用的順序

visit

visitSource?

visitOuterClass?

( visitAnnotation | visitAttribute)*

( visitInnerClass | visitField | visitMethod)*

visitEnd

這就是說,visit 方法必須最先被調用,然後是最多調用一次 visitSource 方法,然後是最多調用一次 visitOuterClass 方法。然後是 visitAnnotation 和 visitAttribute 方法以任意順序被調用任意多次。再然後是以任任意順序調用 visitInnerClass ,visitField 或 visitMethod 方法任意多次。最終,調用一次 visitEnd 方法結束對類的通路,當然我們通過列印的方式或通過官方提供的TraceClassVisitor工具也很容易了解到類資訊的通路順序。

ASM程式設計示例

編寫自己的類通路者,需要繼承自ClassVisitor,定義基類:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
public abstract class AbstractClassVisitor extends ClassVisitor {
    public static final int ASM_API_VERSION = Opcodes.ASM5;
    public AbstractClassVisitor(ClassVisitor delegate) {
        super(ASM_API_VERSION, delegate);
    }
}
           

添加一個屬性

import org.objectweb.asm.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class AddField extends AbstractClassVisitor {
    private boolean isPresent;
    private int access;
    private String name;
    private String desc;
    private Object value;
    public AddField(int access, String name, String desc, Object value, ClassVisitor delegate) {
        super(delegate);
        this.access = access;
        this.name = name;
        this.desc = desc;
        this.value = value;
    }
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (name.equals(this.name)) {
            isPresent = true;
        }
        return super.visitField(access, name, desc, signature, value);
    }
    @Override
    public void visitEnd() {
        if (!isPresent) {
            FieldVisitor fv = super.visitField(access, name, desc, null, value);
            if (fv != null) {
                fv.visitEnd();//不是原有的屬性,故不會有事件發出的,自己 end 掉。
            }
        }
        super.visitEnd();
    }
    public static void main(String[] args) throws Exception {
        ClassReader classReader = new ClassReader(new FileInputStream("TestBean.class"));

        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        ClassVisitor addField = new AddField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
                "newField2",
                Type.getDescriptor(String.class),
                "newValue2",
                classWriter);
        classReader.accept(addField, ClassReader.SKIP_DEBUG);
        byte[] newClass = classWriter.toByteArray();
        File newFile = new File("TestBean.class");
        new FileOutputStream(newFile).write(newClass);
    }
}
           

上述代碼中,使用ClassReader從一個類檔案中讀取到類的二進制形式,ClassWriter用于完成二進制檔案的寫操作,AddField是業務類,其visitEnd()方法被最後觸發,在這裡加入下面的代碼,完成了屬性的添加:

FieldVisitor fv = super.visitField(access, name, desc, null, value);
if (fv != null) {
   fv.visitEnd();//不是原有的屬性,故不會有事件發出的,自己 end 掉。
}
           

如果要删除一個屬性,則在visitField方法中操作,直接傳回null即可,如:

public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
   if (name.equals(this.name)) {
      isPresent = true;
      return null;// 傳回 null 即可删除此屬性
   }
  return super.visitField(access, name, desc, signature, value);
}
           

添加接口

public class AddInterfaces extends AbstractClassVisitor {
    private Set newInterfaces;
    public AddInterfaces(ClassVisitor cv, Set newInterfaces) {
        super(cv);
        this.newInterfaces = newInterfaces;
    }
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        Set<String> ints = new HashSet(newInterfaces);
        ints.addAll(Arrays.asList(interfaces));
        super.visit(version, access, name, signature, superName, ints.toArray(new String[ints.size()]));
    }

    public static void main(String[] args) throws Exception {
        ClassReader cr = new ClassReader(new FileInputStream("TestBean.class"));
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

        Set set = new HashSet();
        set.add(Type.getInternalName(Runnable.class));
        set.add(Type.getInternalName(Comparable.class));

        AddInterfaces cv = new AddInterfaces(cw, set);

        cr.accept(cv, ClassReader.SKIP_DEBUG);

        byte[] bytes = cw.toByteArray();
        // save to disk
        new FileOutputStream(new File("TestBean.class")).write(bytes);
    }
}
           

使用ASM我們幾乎可以操作class的所有資訊,隻要我們知道這些資訊在哪些方法中被觸發,進而我們在合适的時機和地方對其進行相關的操作即可,比如類的版本資訊會在visit方法中被觸發通路,我們就可以在此方法裡對其進行操作,上面的代碼中對TestBean類添加了兩個接口Runnable和Comparable,因為對接口的也通路在visit中被觸發,是以我們在這裡,對其進行了接口的添加:

Set<String> ints = new HashSet(newInterfaces);
        ints.addAll(Arrays.asList(interfaces));
        super.visit(version, access, name, signature, superName, ints.toArray(new String[ints.size()]));
           

當然接口的實作也是需要添加的,這裡省略。另外如果要删除一個類的接口,可以如此:

添加一個方法

public class AddMethod extends AbstractClassVisitor {
    private boolean isInterface;
    private boolean isPresent;
    private String name;
    private String desc;
    public AddMethod(String name, String desc, ClassVisitor delegate) {
        super(delegate);
        this.name = name;
        this.desc = desc;
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        isInterface = (access & ACC_INTERFACE) != ;
        super.visit(version, access, name, signature, superName, interfaces);
    }
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals(this.name) && desc.equals(this.desc)) {
            isPresent = true;
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
    public void visitEnd() {
        if (!isInterface && !isPresent) {
            MethodVisitor mv = super.visitMethod(ACC_PUBLIC + ACC_STATIC, name, desc, null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("liming");
            mv.visitMethodInsn(INVOKESTATIC, "asm/TestBean", "sayHello", "(Ljava/lang/String;)Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l2, );
            mv.visitMaxs(, );//設定ClassWriter.COMPUTE_MAXS 或 ClassWriter.COMPUTE_FRAMES,此處的值會被忽略,但此方法必需顯示調用一下!!!
            mv.visitEnd();
        }
        super.visitEnd();
    }

    public static void main(String[] args) throws Exception {
        ClassReader classReader = new ClassReader(new FileInputStream("TestBean.class"));
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        String methodDescriptor = Type.getMethodDescriptor(Type.getType(Void.class), Type.getType(Integer.class), Type.getType(String.class));
        System.out.println("方法描述符:" + methodDescriptor);
        AddMethod addMethod = new AddMethod("newMethod", methodDescriptor, classWriter);
        classReader.accept(addMethod, ClassReader.SKIP_DEBUG);
        byte[] newClass = classWriter.toByteArray();
        File newFile = new File("TestBean.class");
        new FileOutputStream(newFile).write(newClass);
    }
}
           

要添加一個方法需要對類的位元組碼有較為深入的了解,需要了解其壓棧和出棧的知識以及變量使用等技術,幸運的是ASM提供了TraceClassVisitor工具可以幫助我們輕松擷取這部分的樣例代碼,我們隻需要在上面稍加修改就可以。

删除一個方法同删除屬性的操作類似,在visitMethod處直接傳回null即可:

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
   if (name.equals(mName)) {
      return null;// do not delegate to next visitor -> this removes the method
   }
   return super.visitMethod(access, name, desc, signature, exceptions);
}
           

生成一個完整類并加載

static void generateClass() throws Exception {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(V1_5, ACC_PUBLIC, "pkg/MyObject", null, "java/lang/Object", null);
        cw.visitField(ACC_PUBLIC + ACC_FINAL, "a", "I", null, new Integer(-)).visitEnd();
        cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "b", "I", null, new Integer()).visitEnd();
        cw.visitField(ACC_PUBLIC + ACC_STATIC, "c", "I", null, new Integer()).visitEnd();
        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "other", "I", null, null).visitEnd();
        cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null).visitEnd();
        cw.visitEnd();
        final byte[] bytes = cw.toByteArray();
        // save to disk
        new FileOutputStream(new File("/home/wangpl/Desktop/asm-test/new/MyObject.class")).write(bytes);

        // runtime proxy loader
        class MyClassLoader extends ClassLoader {
            @Override
            protected Class findClass(String name) throws ClassNotFoundException {
                if (name.equals("pkg.MyObject")) {
                    byte[] b = bytes;
                    return defineClass(name, b, , b.length);
                }
                return super.findClass(name);
            }
        }
        Class cls = new MyClassLoader().loadClass("pkg.MyObject");
        // 反射 其屬性 c 的值,上面設定為 100
        System.out.println(cls.getField("c").get(cls));
    }
           

上面的代碼,我們使用ClassWriter,手動觸發需要的方法以驅動其動态生成一個class,觸發的動作原本是ClassReader做的,由于是新生成類,隻能手動調用各方法。

官方代碼生成類

// creates a ClassWriter for the Example public class which inherits from Object
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
        // creates a MethodWriter for the (implicit) constructor
        MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        // pushes the 'this' variable
        mw.visitVarInsn(ALOAD, );
        // invokes the super class constructor
        mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mw.visitInsn(RETURN);
        // this code uses a maximum of one stack element and one local variable
        mw.visitMaxs(, );
        mw.visitEnd();

        // creates a MethodWriter for the 'main' method
        mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        // pushes the 'out' field (of type PrintStream) of the System class
        mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        // pushes the "Hello World!" String constant
        mw.visitLdcInsn("Hello world!");
        // invokes the 'println' method (defined in the PrintStream class)
        mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mw.visitInsn(RETURN);
        // this code uses a maximum of two stack elements and two local variables
        mw.visitMaxs(, );
        mw.visitEnd();
           

列印類的源碼資訊

public class ClassInfoPrinter extends AbstractClassVisitor {
    public ClassInfoPrinter(ClassVisitor delegate) {
        super(delegate);
    }
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return null;
    }
    public void visitInnerClass(String name, String outerName,
                                String innerName, int access) {
    }
    public FieldVisitor visitField(int access, String name, String desc,
                                   String signature, Object value) {
        System.out.println("    " + desc + " " + name);
        return null;
    }
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        System.out.println("    " + name + desc);
        return null;
    }
    public void visitEnd() {
        System.out.println("}");
    }
    public static void main(String[] args) throws Exception {
        ClassReader classReader = new ClassReader(new FileInputStream("/home/wangpl/Desktop/asm-test/TestBean.class"));
        ClassVisitor classVisitor = new ClassInfoPrinter(null);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
    }
}
           

上面的代碼可以列印出TestBean.class的基礎資訊,如類版本、屬性、方法及其描述等等,由于不需要寫操作,故不要代理給ClassWriter。

ASM進行AOP

Aop 無論概念有多麼深奧。它無非就是一個“Propxy模式”。被代理的方法在調用前後作為代理程式可以做一些預先和後續的操作。這一點想必讀者都能達到一個共識。是以要想實作 Aop 的關鍵是,如何将我們的代碼安插到被調用方法的相應位置。

而Aop 最快最直接的方法即是将代碼直接安插到類的相應位置。先看一段最簡單的代碼。

在介紹指令之前我先簡單說明一下 JVM 的運作機制。首先可以簡單的将 JVM 虛拟機看作是一個 CPU。作為 CPU 都要有一個入口程式。在我們的電腦中主機闆的 Bioss 程式就是充當這個角色,而在JVM 中 Main方法來充當這一角色。CPU 在運作程式的時會将程式資料放入它的幾個固定存儲器,我們稱它們為寄存器。CPU 對資料的所有計算都針對寄存器。而 JVM 并不具備這一特征,它采用的是堆結構。

比方說計算 “a + b”,在 CPU 中需要兩個寄存器。首先将“1”載入第一個寄存器,其次将另外一個“1”載入第二個寄存器,然後調用相應的加法指令将兩個寄存器中的資料相加。相加的結果會儲存在另外一個寄存器上。而在 JVM 中首先将第一個“1”push到堆棧中,其次在将另外一個“1”push到堆棧中,緊接着調用ADD指令。這個指令會取出這兩個數字相加然後将結果再次放入堆棧中。經過運算之後堆棧中的兩個“1”已經不存在了,在堆棧頂端有一個新的值“2”。JVM 所有計算都是在此基礎之上完成的。

在 Java 中每一個方法在執行的時候 JVM 都會為其配置設定一個“幀”,幀是用來存儲方法中計算所需要的所有資料。其中第 0 個元素就是 “this”,如果方法有參數傳入會排在它的後面。

ALOAD_0:

這個指令是LOAD系列指令中的一個,它的意思表示裝載目前第 0 個元素到堆棧中。代碼上相當于“this”。而這個資料元素的類型是一個引用類型。這些指令包含了:ALOAD,ILOAD,LLOAD,FLOAD,DLOAD。區分它們的作用就是針對不用資料類型而準備的LOAD指令,此外還有專門負責處理數組的指令 SALOAD。

invokespecial:

這個指令是調用系列指令中的一個。其目的是調用對象類的方法。後面需要給上父類的方法完整簽名。“#8”的意思是 .class 檔案常量表中第8個元素。值為:“java/lang/Object.””:()V”。結合ALOAD_0。這兩個指令可以翻譯為:“super()”。其含義是調用自己的父類構造方法。

GETSTATIC:

這個指令是GET系列指令中的一個其作用是擷取靜态字段内容到堆棧中。這一系列指令包括了:GETFIELD、GETSTATIC。它們分别用于擷取動态字段和靜态字段。

IDC:

這個指令的功能是從常量表中裝載一個資料到堆棧中。

invokevirtual:

也是一種調用指令,這個指令差別與 invokespecial 的是它是根據引用調用對象類的方法。這裡有一篇文章專門講解這兩個指令:“http://wensiqun.iteye.com/blog/1125503”。

RETURN:

這也是一系列指令中的一個,其目的是方法調用完畢傳回:可用的其他指令有:IRETURN,DRETURN,ARETURN等,用于表示不同類型參數的傳回。

詳細參考:http://my.oschina.net/u/1166271/blog/162796

原先的業務代碼示例:

public class TestBean {
    public void halloAop() {
        System.out.println("Hello Aop");
    }
}
           

定義AOP攔截器

public class AopInterceptor {
    public static void beforeInvoke() {
        System.out.println("before");
    }

    public static void afterInvoke() {
        System.out.println("after");
    }

    public static void main(String[] args) throws Exception {
        ClassReader classReader = new ClassReader(new FileInputStream("/home/wangpl/mine/WORK_SPACE/idea_work/Eden/test/target/test-classes/asm/TestBean.class"));
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        ClassVisitor visitor = new AopClassAdapter(Opcodes.ASM5, classWriter);
        classReader.accept(visitor, ClassReader.SKIP_DEBUG);
        new FileOutputStream("/home/wangpl/Desktop/a/TestBean.class").write(classWriter.toByteArray());
    }
}
           

定義AOP的通路者AopClassAdapter,對原類進行泛化:

class AopClassAdapter extends ClassVisitor implements Opcodes {
    public AopClassAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }

    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        //更改類名,并使新類繼承原有的類。
        super.visit(version, access, name + "_Tmp", signature, name, interfaces);
        {
            MethodVisitor mv = super.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, );
            mv.visitMethodInsn(INVOKESPECIAL, name, "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(, );
            mv.visitEnd();
        }
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if ("<init>".equals(name))
            return null;
        if (!name.equals("halloAop"))
            return null;
        //
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        return new AopMethod(this.api, mv);
    }
}
class AopMethod extends MethodVisitor implements Opcodes {
    public AopMethod(int api, MethodVisitor mv) {
        super(api, mv);
    }

    public void visitCode() {
        super.visitCode();
        this.visitMethodInsn(INVOKESTATIC, "asm/AopInterceptor", "beforeInvoke", "()V");
    }

    public void visitInsn(int opcode) {
        if (opcode == RETURN) {
            mv.visitMethodInsn(INVOKESTATIC, "asm/AopInterceptor", "afterInvoke", "()V");
        }
        super.visitInsn(opcode);
    }
}
           

ASM的實用工具

除ClassVisitor、ClassReader、ClassWriter等一些核心接口之外,ASM還提供了一些實用的工具,這些工具在開發class生成器和擴充卡中非常有用。

Type

Type對象代表了一個Java類型,同時也包含了代表了基本類型的靜态變量,如Type.INT_TYPE對應Int,它提供了友善方法可以從在java類型和ASM類型之間的轉換,如Type.getType(String.class).getInternalName()傳回String類的internal name,”java/lang/String”,Type.getType(String.class).getDescriptor()傳回String類的descriptor,”Ljava/lang/String”,Type.getArgumentTypes(“(I)V”)傳回一個包含單個元素類型TYPE.INT_TYPE,Type.getReturnType(“(I)V”)傳回Type.VOID_TYPE對象。

TraceClassVisitor

通過ClassWriter我們無法确定ASM生成的或轉換的class是否符合我們預期,因為ClassWriter傳回的隻是位元組數組不做任何檢查,而TraceClassVisitor提供了文本的展現形式,該類也繼承自ClassVisitor,并代理了所有調用到其他ClassVisitor,在委托給其下一個visitor之前,列印出每次通路操作的全過程,用法如下:

TraceClassVisitor tcv = new TraceClassVisitor(cv, new ASMifier(), new PrintWriter(System.out));
//TraceClassVisitor tcv = new TraceClassVisitor(cv, new Textifier(), new PrintWriter(System.out));
classReader.accept(tcv, ClassReader.SKIP_DEBUG);
           

Tip:

其中使用ASMifier可以展現一個通路者通路具體類資訊的過程(另外一種方式是Textifier),ASMifier在我們自己定制通路者時是十分有用的,比如,我們要生成一個方法就可以先寫一個包行此方法的java類編譯成class,然後使用TraceClassVisitor去列印出這個方法的通路過程,我們就可以清晰地看到生成此方法的通路者邏輯了。

ASMifier也可以通過指令行直接使用,這是因為ASMifier本身包含了main方法入口,内部邏輯和上面的也是一緻的,通過指令行可以不要編寫代碼就檢視類資訊,用法如下:

java -classpath asm.jar:asm-util.jar org.objectweb.asm.util.ASMifier java.lang.Runnable

其中java.lang.Runnable是要檢視的類全名或類檔案的相對路徑。

同樣,Textifier也有類似的用法。

CheckClassAdapter

前面我們已經知道通路者的方法是被ASM按順序觸發執行,尤其是要傳遞到ClassWriter中進行類的組織重寫的時候,順序是很重要的,錯誤的順序可能是ClassWriter寫出類無法使用,然而ClassWriter本身不會檢查調用是否以合适的順序和有效的參數在進行,為了盡可能探測到一些錯誤,可以使用CheckClassAdapter。像TraceClassVisitor一樣,該類也繼承自ClassVisitor,并代理了所有調用到其他ClassVisitor,在委托給其下一個visitor之前,檢查他的方法以合适的順序和有效的參數被調用,用法舉例:

InputStream is = ...; // get bytes for the source class
ClassReader cr = new ClassReader(is);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new MyClassAdapter(new CheckClassAdapter(cw));
cr.accept(cv, ClassReader.SKIP_DEBUG);

StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
CheckClassAdapter.verify(new ClassReader(cw.toByteArray()), false, pw);
assertTrue(sw.toString(), sw.toString().length()==);
           

NOTE:

TraceClassVisitor 多用在自定義ClassVisitor之前以列印正在通路的資訊,而CheckClassAdapter則多用于其後以檢查通路的順序是否正确。

ASM Tree API

Tree API就是針對Class,Method,Field等屬性進行了一個包裝,可以讓我們用面向對像的形式來操作位元組碼,但也不要負于太高的期望,Tree API官方稱會有30%的性能損失,并且需要更多的記憶體開銷,對于一些運作期實時性的位元組碼修改(實時動态代理等)對性能的要求往往會比較高,使用Tree API可能會達不到要求,況且個人實踐發現其宣稱的便利性效果也不是很理想,關于這方面不作深入讨論。

問題

當使用ASM修改位元組碼後有時候不能debug調試,這是因為ASM修改位元組碼的時候丢掉了行号line number資訊,這個設定在 cr.accept(cv, ClassReader.SKIP_DEBUG); 就是這個ClassReader.SKIP_DEBUG參數,當使用 ClassReader.SKIP_DEBUG和ClassReader.SKIP_CODE時都會丢失行号資訊,使用其它的如 SKIP_FRAMES 和 EXPAND_FRAMES 則不會,更保險的 直接 設定為 0,則所有的都不會禁用,即開啟所有class通路。

其它

這裡有一篇IBM的技術文章,比較詳細的介紹了Class規範和ASM的相關技術:

http://www.ibm.com/developerworks/cn/java/j-lo-asm30/

想了解更多:

http://asm.ow2.org/

下一篇: AOP技術研究