在上一篇中有提到spring aop的動態字節碼增強,我自己也沒看過spring 的實現方式,按照大家的說法應該是動態生產一個子類去重寫方法,由于自己沒去看過,暫且不表,接下去,可能還是打算從分析字節碼的角度去看類似于spring aop這個功能反應到字節碼有哪些變化,或者說實現方式,
這個例子還是基于最簡單的HelloWorld,還請大家回顧一下前面的幾個章節,最要是這個 HelloWorld.class 文件的解讀?這個例子和前面兩個例子一下將基于它做一些變化,再去從字節碼的角度去比較看看究竟做了什么。
?
首先考慮aop的一個最簡單應用場景,就是日志輸出,假設現在需要在輸出hello world 的前后都打印日志:代碼如下:
?
?
public class HelloWorld{
public static void main(String [] arvgs){
System.out.println("before log");
System.out.println("hello world");
System.out.println("after log");
}
}
?
?
編譯后的class 文件如下:
?
?
00000000h: CA FE BA BE 00 00 00 32 00 21 0A 00 08 00 11 09 ; 漱壕...2.!......
00000010h: 00 12 00 13 08 00 14 0A 00 15 00 16 08 00 17 08 ; ................
00000020h: 00 18 07 00 19 07 00 1A 01 00 06 3C 69 6E 69 74 ; ...........<init
00000030h: 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 ; >...()V...Code..
00000040h: 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 ; .LineNumberTable
00000050h: 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 ; ...main...([Ljav
00000060h: 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 ; a/lang/String;)V
00000070h: 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0F ; ...SourceFile...
00000080h: 48 65 6C 6C 6F 57 6F 72 6C 64 2E 6A 61 76 61 0C ; HelloWorld.java.
00000090h: 00 09 00 0A 07 00 1B 0C 00 1C 00 1D 01 00 0A 62 ; ...............b
000000a0h: 65 66 6F 72 65 20 6C 6F 67 07 00 1E 0C 00 1F 00 ; efore log.......
000000b0h: 20 01 00 0B 68 65 6C 6C 6F 20 77 6F 72 6C 64 01 ; ...hello world.
000000c0h: 00 09 61 66 74 65 72 20 6C 6F 67 01 00 0A 48 65 ; ..after log...He
000000d0h: 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 2F ; lloWorld...java/
000000e0h: 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 10 6A 61 ; lang/Object...ja
000000f0h: 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 ; va/lang/System..
00000100h: 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69 6F 2F ; .out...Ljava/io/
00000110h: 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 6A ; PrintStream;...j
00000120h: 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 ; ava/io/PrintStre
00000130h: 61 6D 01 00 07 70 72 69 6E 74 6C 6E 01 00 15 28 ; am...println...(
00000140h: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E ; Ljava/lang/Strin
00000150h: 67 3B 29 56 00 21 00 07 00 08 00 00 00 00 00 02 ; g;)V.!..........
00000160h: 00 01 00 09 00 0A 00 01 00 0B 00 00 00 1D 00 01 ; ................
00000170h: 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 ; ......*?.?....
00000180h: 0C 00 00 00 06 00 01 00 00 00 01 00 09 00 0D 00 ; ................
00000190h: 0E 00 01 00 0B 00 00 00 3D 00 02 00 01 00 00 00 ; ........=.......
000001a0h: 19 B2 00 02 12 03 B6 00 04 B2 00 02 12 05 B6 00 ; .?...?.?...?
000001b0h: 04 B2 00 02 12 06 B6 00 04 B1 00 00 00 01 00 0C ; .?...?.?.....
000001c0h: 00 00 00 12 00 04 00 00 00 03 00 08 00 04 00 10 ; ................
000001d0h: 00 05 00 18 00 06 00 01 00 0F 00 00 00 02 00 10 ; ................
?
?
?
接下去首先將這個class文件和原始的HelloWorld的class 的文件對比,為了大家對比方面,把前面的class文件在拿過來:
?
00000000h: CA FE BA BE 00 00 00 32 00 1D 0A 00 06 00 0F 09 ; 漱壕...2........
00000010h: 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07 ; ................
00000020h: 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 ; .....<init>...()
00000030h: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E ; V...Code...LineN
00000040h: 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69 ; umberTable...mai
00000050h: 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 ; n...([Ljava/lang
00000060h: 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 ; /String;)V...Sou
00000070h: 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57 ; rceFile...HelloW
00000080h: 6F 72 6C 64 2E 6A 61 76 61 0C 00 07 00 08 07 00 ; orld.java.......
00000090h: 17 0C 00 18 00 19 01 00 0B 68 65 6C 6C 6F 20 77 ; .........hello w
000000a0h: 6F 72 6C 64 07 00 1A 0C 00 1B 00 1C 01 00 0A 48 ; orld...........H
000000b0h: 65 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 ; elloWorld...java
000000c0h: 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 10 6A ; /lang/Object...j
000000d0h: 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 ; ava/lang/System.
000000e0h: 00 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69 6F ; ..out...Ljava/io
000000f0h: 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 ; /PrintStream;...
00000100h: 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 ; java/io/PrintStr
00000110h: 65 61 6D 01 00 07 70 72 69 6E 74 6C 6E 01 00 15 ; eam...println...
00000120h: 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 ; (Ljava/lang/Stri
00000130h: 6E 67 3B 29 56 00 21 00 05 00 06 00 00 00 00 00 ; ng;)V.!.........
00000140h: 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1D 00 ; ................
00000150h: 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 ; .......*?.?...
00000160h: 00 0A 00 00 00 06 00 01 00 00 00 01 00 09 00 0B ; ................
00000170h: 00 0C 00 01 00 09 00 00 00 25 00 02 00 01 00 00 ; .........%......
00000180h: 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01 00 ; ..?...?.?....
00000190h: 0A 00 00 00 0A 00 02 00 00 00 03 00 08 00 04 00 ; ................
000001a0h: 01 00 0D 00 00 00 02 00 0E ; .........
?
?
這里不會對全部的class 文件進行說明,大家可以參考?HelloWorld.class 文件的解讀?和 helloWorld.class -方法解讀
?
? ?首先從常量池開始看,改動后的HelloWorld(下面稱?HelloWorld后),有32 個常量,而改動前的HelloWorld(下面稱HelloWorld前)只有28個常量,改動后多了4個常量,大家可以假設我們多輸出了兩個字符串,會多兩個常量,那么還有兩個呢?還記得constant_String 類型嗎?或者說有沒有主要到我們一般不會直接使用constant_UTF8類型的值,一般都有一個具體的類型來引用它,所以剩下的兩個便是constant_String 類型的引用,來引用我們要輸出的字符串,那么多了兩個常量對其他的常量會有什么影響呢,很容易想到,其他的常量的引用可能會增加1到4個索引
?
?好了,有了前面的理解,接下去在去仔細看看具體的常量:
1、先看第一號常量:tag=0X 0A 為一個 constant_methodref 類型(對一個類中申明的方法的符號引用),根據它的定義,后面四個字節屬于它,class_index=0X00 08,name_and_type_index=0X00 11 ;和HelloWorld前相比會發現兩個index 都后移2;接下去2號常量一樣也只是所以后偏移了2位
2、接下來看3、4、5、6號常量,對應到HelloWorld前應該是3、4號常量,先看HelloWorld前的意思,3號表示要輸出的字符串,4號表示println方法,在看HelloWorld后,3號表示一個輸出的字符串 “before log”,4號任然表示println方法,5號表示這個字符串“hello world”,6號表示最后一個 “after log”,由于HelloWorld后都是調用println方法,而對這個方法的描述只一次是可以理解的;到這里我們已經找出了2個多出的常量,
3、接下去7、8號常量對應HelloWorld前的5、6號,可以發現也只是index向后偏移4位
4、接下去的9到19號常量對應HelloWorld前的7到17號常量,發現如果是constant_UTF8類型則內容一樣,其他的也只是index 向后偏移2位或者4位
5、接下去看20號到24號常量,對應HelloWorld前18、19、20 號,同樣先看HelloWorld前,18號constant_utf8類型是hello world字符串;19號時對printstream類的引用,20號時對println方法的定義;再看HelloWorld后20號時constant_utf8類型是before log 的字符串,20、22對應HelloWorld前的19、20;23號對應HelloWorld前的18號,24號表示constant_utf8類型的是after log 字符串;這里又有兩個多出來的字符串常量;這樣4個多出來的常量就全部找到了
6、接下去大家可以發現剩下的常量,都是index可能有2位或者4位的偏移;
?
7、接下我們關注?method_info結構,先說第一個method_info,表示的是init方法,我們的改動不會影響這個方法,所有跟前面一樣只是引用常量池的時候index有一些變化,這里不關注;
?
8、接下去就是我們第三個重點了,第二個method_info結構,表示main方法;
(1)、接下去的2個字節表示access_flags=0X 00 09,表示是一個ACC_PUBLIC和ACC_STATIC 的方法,
(2)、在兩個字節0X 00 0D(HelloWorld前為0B,后移2位)表示 name_index,表示常量池中第13個常量為main?,即是?main?方法,
(3)、在接下兩個字節 0X00 0E(HelloWorld前為0C,后移2位) 表示desciptor_index,表示常量池第14個常量為([Ljava/lang/Str?ing;)V,即是參數為String [],返回值為void;
(4)、在接下去兩個字節0X 00 01 表示attribute_count,表示有1個attribute,索引接下去表示一個attribute_info 的結構;所有查看attribute_info 的結構定義
?
?
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
?
?
所以 在接下去的兩個字節 0X 00 0B(HelloWorld前為09,后移2位),查看第11好常量池為Code,然后code_attribute的定義:
?
?
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
?
?
?在看這個結構體 attribute_name_index =0X 00 09,然后4個字節0X 00 00 00 3D(HelloWorld前為25,表示長度 為37個字節) 表示61個字節;這個61個字節是我們關注的;我們單獨拿出來
?
HelloWorld后:
00000199h: 00 02 00 01 00 00 00 19 B2 00 02 12 03 B6 00 04 ; ........?...?.
000001a9h: B2 00 02 12 05 B6 00 04 B2 00 02 12 06 B6 00 04 ; ?...?.?...?.
000001b9h: B1 00 00 00 01 00 0C 00 00 00 12 00 04 00 00 00 ; ?..............
000001c9h: 03 00 08 00 04 00 10 00 05 00 18 00 06 ; .............
?
?對 helloword前的解析參見這里?中的第二塊
?
(1)、0X 00 02 表示max_stack;表示該方法執行的時候操作數棧最大的長度;這里表示操作數棧的長度為2;
(2)、0X 00 01 表示?max_locals;表示方法局部變量所需要的空間的長度
(3)、0X 00 00 00 19 表示code_length=25;即后面的25個字節為code的內容;
(4)、 B2 00 02 12 03 B6 00 04?B2 00 02 12 05 B6 00 04 B2 00 02 12 06 B6 00 04 B1 :25個字節表示的便是code 的內容;
? ? ? 該code[] 包含的實現該方法的JVM 的實際的字節,
?
- 0X B2 :getstatic 指令:表示獲取指定類的靜態域,并將其值壓入棧頂,后面的0X 00 02 ;查看2號常量池,即將out(sysytem.out)壓入棧
- 0X 12 :ldc:表示將一個常量池壓入操作棧,后面的0X 03 便是這個操作數,查看第3號常量池,為berfore log,我們要輸出的內容;
- 0X B6 : invokevirtual,調用實例方法,后面的0X 00 04 ,查看4號常量池 表示java/io/PrintStream的println方法,這個指令彈出兩個操作數,即是調用 out.print("before log");
-
0X B2 :getstatic 指令:表示獲取指定類的靜態域,并將其值壓入棧頂,后面的0X 00 02 ;查看2號常量池,即將out(sysytem.out)壓入棧
- 0X 12 :ldc:表示將一個常量池壓入操作棧,后面的0X 05 便是這個操作數,查看第5號常量池,為hello world,我們要輸出的內容;
- 0X B6 : invokevirtual,調用實例方法,后面的0X 00 04 ,查看4號常量池 表示java/io/PrintStream的println方法,這個指令彈出兩個操作數,即是調用 out.print("hello world");
- 0X B2 :getstatic 指令:表示獲取指定類的靜態域,并將其值壓入棧頂,后面的0X 00 02 ;查看2號常量池,即將out(sysytem.out)壓入棧
- X 12 :ldc:表示將一個常量池壓入操作棧,后面的0X 06 便是這個操作數,查看第3號常量池,為after log,我們要輸出的內容;
- X B6 : invokevirtual,調用實例方法,后面的0X 00 04 ,查看4號常量池 表示java/io/PrintStream的println方法,這個指令彈出兩個操作數,即是調用 out.print("after log");
- 0X?B1 : return ;返回void
(5)、0X 00 00 :表示exception_table_length=0;也就是說沒有異常處理;
(6)、0X 00 01 :表示attributes_count=1;接下來有一個attribute_info 的結構:
? ?1)、0X 00 0C (HelloWorld前為0A,后移2位) :表示?attribute_name_index,查看10號常量池,為LineNumberTable ;
查 看LineNumberTable ?屬性的定義:
?
? ?2)、0X 00 00 00 12 (HelloWorld前為0A) :表示attribute_length=18,
? ?3)、0X 00 04 :表示line_number_table_length=4,即后面有4個line_number_info 結構
? ? ? ?3.1)、0X 00 00 表示 start_pc;新行開始時,代碼數組的偏移量,該偏移量從代碼數組的起始位置開始;
? ? ? ?3.2)、0X 00 03 表示 line_number=3
?
? ? ? ?3.3)、0X 00 08 表示 start_pc;新行開始時,代碼數組的偏移量,該偏移量從代碼數組的起始位置開始;
? ? ? ?3.4)、0X 00 04 表示 line_number=4
?
? ? ? ?3.5)、0X 00 10表示 start_pc;新行開始時,代碼數組的偏移量,該偏移量從代碼數組的起始位置開始;
? ? ? ?3.6)、0X 00 05 表示 line_number=5
?
? ? ? ?3.7)、0X 00 18 表示 start_pc;新行開始時,代碼數組的偏移量,該偏移量從代碼數組的起始位置開始;
? ? ? ?3.8)、0X 00 06 表示 line_number=6
?
LineNumberTable ?中包含了一些調試信息,不做討論;
?
這樣main方法就好了;
?
9、接下去表示SourceFile屬性,不去關注;
?
10、終結一下我們比較的結構,可以發現首先是在常量池中會增加4個常量,這是由于我們多輸出了兩個字符串引發的,接著由于這4個常量的出現,打亂了原來常量池的順序,導致索引大量向后偏移;最后就是main方法的coed 的字節碼增加了,由原來的9個增加的 25個;再仔細看原來這個9個字節,其實前8個是方法體,最后一個是return;所以當我們增加了2個輸出語句,這樣3*8=24 再加1個返回就是25個字節了
?
最后,我們考慮如果我們需要用這種字節碼增強的方式去實現aop的話,那么最大的麻煩在于需要后移原來的常量池的索引,如果能夠保持原來的常量池中的常量的位置,新增的常量只是加在最后面的話,這樣就可以省去大量的工作,下一篇我們將嘗試用這種方法去直接修改二進制碼來嘗試;在下去希望可以通過程序實現我們手工做的事情;