ASM(一) 利用Core API 解析和生成字節碼


 

     ASM是一個提供字節碼解析和操作的框架。Cglib框架就是基於ASM框架實現的,被廣泛應用的Hibernate,Spring就是基於Cglib 實現了AOP技術。

    在說到AOP的Java實現,可能會優先想到java的Proxy api,通過invoke方法攔截處理相應的代碼邏輯,但是proxy 是面向接口的,被代理的class的所有方法調用都會通過反射調用invoke 方法,相對性能開銷大。另外的還有Java 5提供的Instrument,比較適用於監控檢查方面,但在處理靈活的代碼邏輯方面並不合適。

    ASM 框架對用戶屏蔽了整個類字節碼的長度,偏移量,能夠更加靈活和方便得實現對字節碼的解析和操作。其主要提供了兩部分主要的API,Core Api 及Tree Api。本文先從Core Api的解析和生成字節碼開始介紹。

   CoreApi 在解析和生成這裏,主要用的的類是ClassVisitor。ClassVisitor在2.0版本的時候是個接口,對於class文件操作都是訪問ClassAdepter。這裏主要基於4.0以後的API做介紹。

   先看一下ClassVisitor代碼:

public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,
String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
void visitEnd();
}


    ClassVisitor 的調用必須是遵循下面的調用順序的:

  visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd



   圍繞着ClassVisitor ,還有兩個核心類:
   後續的例子代碼中可以看到,我們必須先調用visit方法,這就因爲class是字節流的二進制文件,而我們解析和生成也是要遵循一定的順序。ClassVisitor定義了我們需要操作的所有接口,並且ClassVisitor也可以接收一個ClassVisitor實例來構造,有點類似於一個事件的filter,可以套很多層的filter來一層層處理邏輯。

1、ClassReader 將class解析成byte 數組,然後會通過accept方法去按順序調用綁定對象(繼承了ClassVisitor的實例)的方法。可以視爲一個事件的生產者。

2、ClassWriter 是ClassVisitor 的子類。直接可以通過toByteArray()方法以返回的byte數組形式構建編譯後的class。可以視爲一個事件的消費者。

   下面來看下具體這三個Api的應用。

一、解析

    因爲Java編譯後的class文件就是由特定格式的字節流組成的二進制文件。這裏不展開說明class文件結構。那麼我們可以通過ASM中的ClassReader 類來解析類的方法,屬性,註解,父類和接口信息,內部類等等。下面這個例子主要示範下打印類的一些屬性、方法信息(javap 工具類似功能)。

 

Task 類是我們需要解析的類。

package asm.core;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class Task {
 
    private int isTask = 0;
 
    private long tell = 0;
 
    public void isTask(){
        System.out.println("call isTask");
    }
    public void tellMe(){
        System.out.println("call tellMe");
    }
}



ClassPrintVisitor 類繼承自ClassVisitor類來打印解析類的類名,父類名以及“is”開頭的屬性和方法。
 

package asm.core;
 
import org.objectweb.asm.*;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class ClassPrintVisitor extends ClassVisitor {
 
 
    public ClassPrintVisitor() {
        super(Opcodes.ASM4);
    }
 
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
 
    public FieldVisitor visitField(int access, String name, String desc,
                                   String signature, Object value) {
        if (name.startsWith("is")) {
            System.out.println(" field name: " + name + desc);
        }
        return null;
    }
 
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        if (name.startsWith("is")) {
            System.out.println(" start with is method: " + name + desc);
        }
        return null;
    }
 
    public void visitEnd() {
        System.out.println("}");
    }
}



下面是測試類ClassesPrintTest 。將一個ClassPrintVisitor 對象傳給ClassReader。ClassReader作爲一個解析事件的producer 並且由ClassPrintVisitor去消費(處理打印邏輯)。accept()方法就將Task 字節碼進行解析,然後調用ClassPrintVisitor 的方法。
 

package asm.core;
 
import org.objectweb.asm.ClassReader;
 
import java.io.IOException;
 
/**
 * Created by yunshen.ljy on 2015/6/8.
 */
public class ClassesPrintTest {
 
    public static void main(String[] args) {
        try {
            ClassReader cr = new ClassReader("asm.core.Task");
            ClassPrintVisitor cp = new ClassPrintVisitor();
            cr.accept(cp, 0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


調用結果:

asm/core/Task extends java/lang/Object {
 field name: isTaskI
 start with is method: isTask()V
}


    這裏我們打印了is開頭的屬性名以及描述,方法名以及描述。這裏的desc其實是class文件屬性修飾或者方法參數、返回值的全限定名(fully qualified name)。這裏isTask後面的I 代表的是int類型的描述。IsTask()方法後面的V表示返回值是void。詳細的屬性、方法描述可以參考《The Java Virtual Machine Specification》

   

二、生成

         生成字節碼,聽起來有點暴力。Java編譯後的class文件是以字節流的形式組成。所以也就是生成一個byte數組,就是之前說的以“特定格式”生成一個可被JVM加載、執行的字節流。下面這個例子是生成一個類ChildClass,繼承自ParentInter 接口,這個是個空接口(沒有具體變量和方法,只是爲了示範代碼)。我們用ASM 的ClassWriter 來實現。先看一下ChildClass 類的代碼:

package asm.core;
 
import asm.core.ParentInter;
 
public abstract class ChildClass implements ParentInter {
    public static final int zero = 0;
 
    public abstract int compareTo(Object var1);
}



  
我們通過調用ClassVisitor的方法就能實現生成上述代碼。 

private static byte[] gen() {
        ClassWriter cw = new ClassWriter(0);
        cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT ,
                "asm/core/ChildClass", null, "java/lang/Object", new String[]{"asm/core/ParentInter"});
      
 cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "zero", "I", null, new Integer(0))
                .visitEnd();
     
  cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null)
                .visitEnd();
        cw.visitEnd();
        return cw.toByteArray();
    }


     生成一個能代表class文件的字節數組,我們需要先調用visit 方法生成一個class的頭部信息。這裏的幾個參數稍作解釋:第一個參數Opcode是ASM內部定義的操作數常量,這裏的V1_5代表Java1.5,第二個參數代表類的修飾符,這裏是一個抽象類。第三個是類名。第四個參數本例傳null ,因爲這裏我們沒有一個類型參數。後面兩個是父類和接口。visitField()和visitMethod()方法分別生成我們的屬性和方法,這裏看到都會再調用visitEnd方法,是因爲我們這裏的field後續沒有相應visitAnnotation 、visitAttribute等方法調用,method 後續也沒有其他調用。最後調用cw.visitEnd()結束整個創建過程,toByteArray()返回了我們需要的代表這個類的byte 數組。
     這裏我們是new 一個ClassWriter。ClassWriter 繼承自ClassVisitor。前面已經介紹過ClassVisitor。這裏ClassWriter 的toByteArray() 返回的字節數組就能代表一個我們生成的class。

    通過這個byte數組我們可以通過ClassLoader來加載我們的類,也可以用FileOutputStream來生成個class文件。

   

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章