使用ASM實現AOP(1)

轉自:http://my.oschina.net/u/1166271/blog/162796?p=2#comments


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

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

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

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

    可以負責任的告訴大家,ASM只不過是通過 “Visitor” 模式將 “.class” 類文件的內容從頭到尾掃描一遍。因此如果你抱着任何更苛刻的要求最後都將失望而歸。上面我們介紹的那些 Aop 框架它們幾乎都屬於 ASM 框架的泛生品。

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

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

?
1
2
3
4
5
publicclass TestBean {
    publicvoid halloAop() {
        System.out.println("Hello Aop");
    }
}

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

?
1
2
3
4
5
6
7
publicclass TestBean {
    publicvoid halloAop() {
        System.out.println("before");
        System.out.println("Hello Aop");
        System.out.println("after");
    }
}

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ javap -c TestBean
Compiled from "TestBean.java"
publicclass org.more.test.asm.TestBean extendsjava.lang.Object{
publicorg.more.test.asm.TestBean();
  Code:
   0:   aload_0
   1:   invokespecial   #8;//Method java/lang/Object."<init>":()V
   4:  return
 
publicvoid 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目的就達到了。

        在介紹指令之前我先簡單說明一下 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 不同的切點,分別是調用前和調用後,攔截器代碼如下:

?
1
2
3
4
5
6
7
8
publicclass AopInterceptor {
    publicstatic void beforeInvoke() {
        System.out.println("before");
    };
    publicstatic void afterInvoke() {
        System.out.println("after");
    };
}

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

?
1
2
3
4
5
6
7
publicclass TestBean {
    publicvoid halloAop() {
        AopInterceptor.beforeInvoke();
        System.out.println("Hello Aop");
        AopInterceptor.afterInvoke();
    }
}

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
mv = cw.visitMethod(ACC_PUBLIC, "halloAop","()V",null,null);
mv.visitCode();
Label l0 = newLabel();
mv.visitLabel(l0);
mv.visitLineNumber(24, l0);
mv.visitMethodInsn(INVOKESTATIC,"org/more/test/asm/AopInterceptor","beforeInvoke","()V");
Label l1 = newLabel();
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 = newLabel();
mv.visitLabel(l2);
mv.visitLineNumber(26, l2);
mv.visitMethodInsn(INVOKESTATIC,"org/more/test/asm/AopInterceptor","afterInvoke","()V");
Label l3 = newLabel();
mv.visitLabel(l3);
mv.visitLineNumber(27, l3);
mv.visitInsn(RETURN);
Label l4 = newLabel();
mv.visitLabel(l4);
mv.visitLocalVariable("this","Lorg/more/test/asm/TestBean;",null, l0, l4, 0);
mv.visitMaxs(2,1);
mv.visitEnd();
}<span></span>

    上面這段代碼的作用是用 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 行看到如下內容:

?
1
2
3
Label l2 = newLabel();
mv.visitLabel(l2);
mv.visitLineNumber(26, l2);

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

?
1
2
3
Label l4 = newLabel();
mv.visitLabel(l4);
mv.visitLocalVariable("this","Lorg/more/test/asm/TestBean;",null, l0, l4, 0);
也是可以被刪除不用的,這部分代碼表示向 class 文件中寫入方法本地變量表的名稱以及類型。經過精簡之後就是下面的代碼了:
?
1
2
3
4
5
6
7
8
9
10
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 代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
classAopClassAdapter extendsClassVisitor implementsOpcodes {
    publicAopClassAdapter(intapi, ClassVisitor cv) {
        super(api, cv);
    }
    publicvoid visit(intversion, intaccess, 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();
        }
    }
    publicMethodVisitor visitMethod(intaccess, String name,
                             String desc, String signature, String[] exceptions) {
        if("<init>".equals(name))
            returnnull;//放棄原有類中所有構造方法
        if(!name.equals("halloAop"))
            returnnull;// 只對halloAop方法執行代理
        //
        MethodVisitor mv = super.visitMethod(access, name,
                                          desc, signature, exceptions);
        returnnew AopMethod(this.api, mv);
    }
}
classAopMethod extendsMethodVisitor implementsOpcodes {
    publicAopMethod(intapi, MethodVisitor mv) {
        super(api, mv);
    }
    publicvoid visitCode() {
        super.visitCode();
        this.visitMethodInsn(INVOKESTATIC,"org/more/test/asm/AopInterceptor","beforeInvoke","()V");
    }
    publicvoid visitInsn(intopcode) {
        if(opcode == RETURN) {//在返回之前安插after 代碼。
            mv.visitMethodInsn(INVOKESTATIC,"org/more/test/asm/AopInterceptor","afterInvoke","()V");
        }
        super.visitInsn(opcode);
    }
}

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

?
1
2
3
4
5
6
7
8
ClassWriter cw = newClassWriter(0);
//
InputStream is = Thread.currentThread().getContextClassLoader()
          .getResourceAsStream("org/more/test/asm/TestBean.class");
ClassReader reader = newClassReader(is);
reader.accept(newAopClassAdapter(ASM4, cw), ClassReader.SKIP_DEBUG);
//
byte[] code = cw.toByteArray();
接下來編寫一個 ClassLoader 加載我們的新類就可以了,新類的名稱後面多了“_Tmp”。本文的所有代碼可以在下面這個地址中得到:

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


發佈了26 篇原創文章 · 獲贊 11 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章