深刻字節碼 -- 使用 ASM 實現 AOP

    AOP 的概念已經不是什麼新鮮事物,因此我在這裏就不在介紹 Aop 的概念。目前市面上要作到 Aop 是一件十分簡單的事情。Spring、AspectJ、CGLib等等均可以幫助你達到目的,可是它們也只不過是一些泛生品。 html

    上面提到了一些開源的 Aop 實現技術選型,可是我敢說不管你嘗試使用上面哪一種技術選型都沒有我將要介紹的這種方式的運行效率最高。不過讀者不要高興的太早,讀完本文想必你就知道是什麼緣由了。 java

    介紹一款工具ASM,下面是(http://www.ibm.com/developerworks/cn/java/j-lo-asm30/)內容的一個節選。 ios

    ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者加強既有類的功能。ASM 能夠直接產生二進制 class 文件,也能夠在類被加載入 Java 虛擬機以前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的全部元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,可以改變類行爲,分析類信息,甚至可以根據用戶要求生成新類。

    能夠負責任的告訴你們,ASM只不過是經過 「Visitor」 模式將 「.class」 類文件的內容從頭至尾掃描一遍。所以若是你抱着任何更苛刻的要求最後都將失望而歸。上面咱們介紹的那些 Aop 框架它們幾乎都屬於 ASM 框架的泛生品。 api

    衆所周知,Aop 不管概念有多麼深奧。它無非就是一個「Propxy模式」。被代理的方法在調用先後做爲代理程序能夠作一些預先和後續的操做。這一點想必讀者都能達到一個共識。所以要想實現 Aop 的關鍵是,如何將咱們的代碼安插到被調用方法的相應位置。 數組

    而要追求 Aop 最快的效率的方法也正式將咱們要執行的代碼直接安插到相應的位置。先看一段最簡單的代碼。 架構

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

    接下來我要爲 halloAop 這個方法加裝一個Aop,使在它以前和以後各打印一段字符串。最後的代碼執行起來看上去應該是這個樣子的: 框架

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

    首先使用 javac 上面類,而後經過javap -c 查看它們的代碼: eclipse

$ javap -c TestBean
Compiled from "TestBean.java"
public class org.more.test.asm.TestBean extends java.lang.Object{
public org.more.test.asm.TestBean();
  Code:
   0:   aload_0
   1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
   4:   return

public void halloAop();
  Code:
   0:   getstatic       #15; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc     #21; //String Hello Aop
   5:   invokevirtual   #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   return
}

    第 04 行:這一行開始到第 08 行 表示的是一個的默認構造方法,雖然咱們沒有在 TestBean 類中編寫任何構造方法,可是做爲 Java 類都應當有一個默認的無參構造方法,而這個構造方法是編譯器爲咱們自動添加的。
    第 10 行:從這行開始到結束就是咱們編寫的 halloAop 方法,下面將會介紹一下上面出現的幾個字節碼指令。
工具

    下面若是咱們能夠在上面字節碼的12行和15行動態的插入代碼那麼咱們的AOP目的就達到了。
this

        在介紹指令以前我先簡單說明一下 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."<init>":()V」。結合ALOAD_0。這兩個指令能夠翻譯爲:「super()」。其含義是調用本身的父類構造方法。

GETSTATIC
    這個指令是GET系列指令中的一個其做用是獲取靜態字段內容到堆棧中。這一系列指令包括了:GETFIELD、GETSTATIC。它們分別用於獲取動態字段和靜態字段。

IDC:
    這個指令的功能是從常量表中裝載一個數據到堆棧中。

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

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

    講了這麼多指令想必已經有不少同窗開始打退堂鼓了。沒錯,ASM 就是讓咱們直接面對底層字節碼。要追求最快的 AOP 執行效率也要從字節碼入手。不過爲了方便開發,我再介紹一個工具,ASM-Bytecode。它是一個Eclipse插件,專門用於 ASM 框架下開發的輔助工具。它能夠幫助咱們生成一些繁瑣的代碼,從而讓咱們儘可能繞開對底層組合虛擬機指令的關心。插件更新地址:「http://andrei.gmxhome.de/eclipse/」,項目首頁:「http://andrei.gmxhome.de/bytecode/index.html

    下面設計一個簡單的架構。讓任何一個被代理的類在其方法調用以前和返回以後都要調用咱們的一個靜態方法。爲了區別它們分別使用兩個方法來表明 Aop 不一樣的切點,分別是調用前和調用後,攔截器代碼以下:

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

    接下來只須要在代理類的方法中插入對這兩個方法的調用便可。首先設想被代理的方法最終執行的代碼應該是下面這個樣子的:

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

使用 ASM-Bytecode 工具,將這段代碼轉變爲 ASM 代碼。下面是上面這段代碼的轉換結果:

{
mv = cw.visitMethod(ACC_PUBLIC, "halloAop", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(24, l0);
mv.visitMethodInsn(INVOKESTATIC, "org/more/test/asm/AopInterceptor", "beforeInvoke", "()V");
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(25, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello Aop");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(26, l2);
mv.visitMethodInsn(INVOKESTATIC, "org/more/test/asm/AopInterceptor", "afterInvoke", "()V");
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(27, l3);
mv.visitInsn(RETURN);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lorg/more/test/asm/TestBean;", null, l0, l4, 0);
mv.visitMaxs(2, 1);
mv.visitEnd();
}

    上面這段代碼的做用是用 ASM 輸出整個 helloAop 的字節碼部分,所以這是一段參考代碼。
    第 02 行:表示準備輸出一個共有方法 「halloAop」,ACC_PUBLIC 表示共有,至關於 public 修飾符。「()V」 是方法的參數包括返回值簽名。「V」 是 void 的縮寫。表示無返回值。後面兩個 null 分別是方法的異常拋出信息和屬性信息。
    第 03 行:表示開始正式輸出方法的執行代碼。
    第 07 行:表示調用靜態方法,這行代碼至關於「AopInterceptor.beforeInvoke();」,這個代碼是咱們須要的。同理第 17 行代碼也是咱們須要的。它至關於「AopInterceptor.afterInvoke();」。

    在上面生成的代碼中 4,5,6,8,9,10,14,15,16,18,19,20 行看到以下內容:

Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(26, l2);

這些內容表示 Java 代碼的行號標記,能夠刪除不用。在方法的最後部分代碼:

Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lorg/more/test/asm/TestBean;", null, l0, l4, 0);
也是能夠被刪除不用的,這部分代碼表示向 class 文件中寫入方法本地變量表的名稱以及類型。通過精簡以後就是下面的代碼了:
mv = cw.visitMethod(ACC_PUBLIC, "halloAop", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "org/more/test/asm/AopInterceptor", "beforeInvoke", "()V");
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello Aop");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mv.visitMethodInsn(INVOKESTATIC, "org/more/test/asm/AopInterceptor", "afterInvoke", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();

逐句對應解釋:
    01 行:至關於 public void halloAop() 方法聲明。
    02 行:正式開發方法內容的填充。
    03 行:調用靜態方法,至關於:「AopInterceptor.beforeInvoke();」。
    04 行:取得一個靜態字段將其放入堆棧,至關於「System.out」。「Ljava/io/PrintStream;」是字段類型的描述,翻譯過來至關於:「java.io.PrintStream」類型。在字節碼中凡是引用類型均由「L」開頭「;」結束表示,中間是類型的完整名稱。
    05 行:將字符串「Hello Aop」放入堆棧,此時堆棧中第一個元素是「System.out」,第二個元素是Hello Aop
    06 行:調用PrintStream類型的「println」方法。簽名「(Ljava/lang/String;)V」表示方法須要一個字符串類型的參數,而且無返回值。
    07 行:調用靜態方法,至關於:「AopInterceptor.afterInvoke();」。
    08 行:是 JVM 在編譯時爲方法自動加上的「return」指令。該指令必須在方法結束時執行不可缺乏。
    09 行:表示在執行這個方法期間方法的堆棧空間最大給予多少。
    10 行:表示方法輸出結束。

下面就是安插 Aop 實現的 ASM 代碼:

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, 0);
            mv.visitMethodInsn(INVOKESPECIAL, name, "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            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;// 只對halloAop方法執行代理
        //
        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, "org/more/test/asm/AopInterceptor", "beforeInvoke", "()V");
    }
    public void visitInsn(int opcode) {
        if (opcode == RETURN) {//在返回以前安插after 代碼。
            mv.visitMethodInsn(INVOKESTATIC, "org/more/test/asm/AopInterceptor", "afterInvoke", "()V");
        }
        super.visitInsn(opcode);
    }
}

接下來就是使用 ASM 改寫 Java 類:

ClassWriter cw = new ClassWriter(0);
//
InputStream is = Thread.currentThread().getContextClassLoader()
          .getResourceAsStream("org/more/test/asm/TestBean.class");
ClassReader reader = new ClassReader(is);
reader.accept(new AopClassAdapter(ASM4, cw), ClassReader.SKIP_DEBUG);
//
byte[] code = cw.toByteArray();
接下來編寫一個 ClassLoader 加載咱們的新類就能夠了,新類的名稱後面多了「_Tmp」。本文的全部代碼能夠在下面這個地址中獲得:

http://www.oschina.net/code/snippet_1166271_24995

後續博文:

  1. 深刻字節碼 -- 使用 ASM 實現 AOP
  2. 深刻字節碼 -- 玩轉 ASM-Bytecode
  3. 深刻字節碼 -- ASM 關鍵類型 ClassVisitor
  4. 深刻字節碼 -- ASM 關鍵接口 MethodVisitor
  5. 深刻字節碼 -- 推薦的 ASM 開發步驟
  6. 未完待續...
相關文章
相關標籤/搜索