ASM Bytecode Framework探索與使用

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() 方法可以獲取生成的字節碼數據。然后使用ClassLoaderdefineClass()方法進行反射實例化對象,并調用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

歡迎關注我的個人訂閱號

個人訂閱號
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,973評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,466評論 25 708
  • 寫在前面: 大學軍訓的確是讓人可以懷念的,這里充滿著歡歌笑語,也有著各種心酸苦辣,但這也告訴我們不再是小孩子,可以...
    穿著西裝去買菜閱讀 475評論 0 1
  • 我想再看一場壩壩電影。 六月,夜晚,這時天已全黑。沒有云霧沒有雜質,純粹的青黑的天純粹得像塊無邊的墨錦...
    soulSHIYU閱讀 356評論 0 2
  • 我最近換了一條上班的路線 要穿過一個村莊 沿著村路蜿蜒向前 絕不是筆直的那種 路的兩旁有好多煙火的氣息 我放慢了車...
    三萬夜閱讀 375評論 5 4