ASM是一款基于java字節碼層面的代碼分析和修改工具。無需提供源代碼即可對應用嵌入所需debug代碼,用于應用API性能分析。ASM可以直接產生二進制class文件,也可以在類被加入JVM之前動態修改類行為。
ASM庫的結構
- Core 為其他包提供基礎的讀、寫、轉化Java字節碼和定義的API,并且可以生成Java字節碼和實現大部分字節碼的轉換
- Tree提供了Java字節碼在內存中的表現
- Analysis為存儲在tree包結構中的java方法字節碼提供基本的數據流統計和類型檢查算法
- Commons提供一些常用的簡化字節碼生成轉化和適配器
- Util包含一些幫助類和簡單的字節碼修改,有利于在開發或者測試中使用
- XML提供一個適配器將XML和SAX-comliant轉化成字節碼結構,可以允許使用XSLT去定義字節碼轉化。
class文件結構
在了解ASM之前,有必要先了解一下class文件結構。對于每個class文件其實都是有固定的結構信息,而且保留了源碼文件中的符號。下圖是class文件的格式圖。其中帶 * 號的表示可重復的結構。
- 類結構體中所有的修飾符、字符常量和其他常量都被存儲在class文件開始的一個常量堆棧(Constant Stack)中,其他結構體通過索引引用。
- 每個類必須包含headers(包括:class name, super class, interface, etc.)和常量堆棧(Constant Stack)其他元素,例如:字段(fields)、方法(methods)和全部屬性(attributes)可以選擇顯示或者不顯示。
- 每個字段塊(Field section)包括名稱、修飾符(public, private, etc.)、描述符號(descriptor)和字段屬性。
- 每個方法區域(Method section)里面的信息與header部分的信息類似,信息關于最大堆棧(max stack)和最大本地變量數量(max local variable numbers)被用于修改字節碼。對于非abstract和非native的方法有一個方法指令表,exceptions表和代碼屬性表。除此之外,還可以有其他方法屬性。
- 每個類、字段、方法和方法代碼的屬性有屬于自己的名稱記錄在類文件格式的JVM規范的部分,這些屬性展示了字節碼多方面的信息,例如源文件名、內部類、簽名、代碼行數、本地變量表和注釋。JVM規范允許定義自定義屬性,這些屬性會被標準的VM(虛擬機)忽略,但是可以包含附件信息。
- 方法代碼表包含一系列對java虛擬機的指令。有些指令在代碼中使用偏移量,當指令從方法代碼被插入或者移除時,全部偏移量的值可能需要調整。
基于事件字節碼處理
在Core包中邏輯上分為2部分:
- 字節碼生產者,例如ClassReader
- 字節碼消費者,例如writers(ClassWriter, FieldWriter, MethodWriter和AnnotationWriter),adapters(ClassAdapter和MethodAdapter)
下圖是生產者和消費者交互的時序圖:
通過時序圖可以看出ASM在處理class文件的整個過程。ASM通過樹這種數據結構來表示復雜的字節碼結構,并利用Push模型來對樹進行遍歷。
- ASM中提供一個
ClassReader
類,這個類可以直接由字節數組或者class文件間接的獲得字節碼數據。它會調用accept
方法,接受一個實現了抽象類ClassVisitor
的對象實例作為參數,然后依次調用ClassVisitor
的各個方法。字節碼空間上的偏移被轉成各種visitXXX方法。使用者只需要在對應的的方法上進行需求操作即可,無需考慮字節偏移。 - 這個過程中
ClassReader
可以看作是一個事件生產者,ClassWriter繼承自ClassVisitor抽象類,負責將對象化的class文件內容重構成一個二進制格式的class字節碼文件,ClassWriter
可以看作是一個事件的消費者。
原java類型與class文件內部類型對應關系
Java type | Type descriptor |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
原java方法聲明與class文件內部聲明的對應關系
Method declaration in source file | Method descriptor |
---|---|
void method(String str,int i,float f) | (Ljava/lang/String;IF)V |
Object method(byte [] b) | ([B)Ljava/lang/Object; |
int[] method(double d) | (D)[I |
遍歷CLASS字節碼類信息
以java.lang.Runnable作為例子
輸出:
superName=java/lang/Object,name=java/lang/Runnable
run()V
end
ClassReader類的accept方法中,有個int類型的flag參數有以下幾種:
- SKIP_DEBUG 用于忽略debug信息,例如,源文件,行數和變量信息。
- SKIP_FRAMES 用于忽略StackMapTable(棧圖)信息。Java 6 之后JVM引入棧圖概念。
- EXPAND_FRAMES 擴展StackMapTable數據,允許訪問者獲取全部本地變量類型與當前堆棧位置的信息。
- SKIP_CODE 排除代碼訪問的所有方法,同時還通過方法參數屬性和注釋。
通過ASM生產自定義類對應的class
目標class內容:
生產目標class的代碼:
這里需要注意,平時我們寫類的時候,默認的構造方法是可以不寫的,但使用ASM框架生產class的話,默認的構造方法是需要寫的,不然,無法實例化對象。
創建類、構造函數與字段:
創建showInfo方法
創建get、set方法
最后生產出Person.class之后,我們可以使用JD-GUI打開:
動態加載生產出的class字節碼并實例化該類
我們可以通過ClassWriter
中的toByteArray()
方法可以獲取生成的字節碼數據。然后使用ClassLoader
的defineClass()
方法進行反射實例化對象,并調用showInfo()
方法。
動態修改class字節碼,進行AOP編程
通過加載上面生成的Person.class
文件,在showInfo()
方法里面添加一行打印當前時間。
通過繼承ClassVisitor,重寫visitMethod()
,攔截showInfo()
方法。
然后讓繼承AdviceAdapter
的類中的onMethodEnter()
方法修改showInfo()
方法。
這樣就可以實現修改class字節碼的操作了。重新生成class文件。使用JD-GUI驗證一下。不出意料,結果是我們所預期的。
雖然例子簡單,但是是進行AOP“無損注入”的基礎展示。著名的Spring框架也是利用這種技術實現AOP的。至此,對ASM框架的一些簡單的使用就是這樣了,其中會涉及到一些JVM操作的理解,可以查看我的另一篇文章:JVM指令
另外,可以到github倉庫查看本次的demo工程:ASMTest
歡迎關注我的個人訂閱號