前言
前面一篇文章 ASM 簡介 對 ASM 框架做了簡單的介紹。
本篇文章主要對該框架的 Core Api 其中重要的一些類進行詳細的介紹,讓大家可以更得心應(yīng)手的使用 ASM。
在開始之前,讓我們先回顧一下 ASM Core Api 調(diào)用流程:
ASM 提供了一個類
ClassReader可以方便地讓我們對class文件進行讀取與解析;ASM 在
ClassReader解析class文件過程中,解析到某一個結(jié)構(gòu)就會通知到ClassVisitor的相應(yīng)方法(eg:解析到類方法時,就會回調(diào)ClassVisitor.visitMethod方法);可以通過更改
ClassVisitor中相應(yīng)結(jié)構(gòu)方法返回值,實現(xiàn)對類的代碼切入(eg:更改ClassVisitor.visitMethod()方法的默認返回值MethodVisitor實例,通過操作該自定義MethodVisitor從而實現(xiàn)對原方法的改寫);其它的結(jié)構(gòu)遍歷也如同
ClassVisitor;通過
ClassWriter的toByteArray()方法,得到class文件的字節(jié)碼內(nèi)容,最后通過文件流寫入方式覆蓋掉原先的內(nèi)容,實現(xiàn)class文件的改寫。
以上,就是 ASM Core Api 的整體運作流程。
接下來,我將對其中涉及到的重要的類進行詳細解析。
ClassReader

這個類會提供你要轉(zhuǎn)變的類的字節(jié)數(shù)組,它的accept方法,接受一個具體的ClassVisitor,并調(diào)用實現(xiàn)中具體的 visit,
visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass,visitField, visitMethod和 visitEnd 方法。
ClassReader.accept(ClassVisitor classVisitor, int parsingOptions)中,第二個參數(shù)parsingOptions的取值有以下選項:
-
ClassReader.SKIP_DEBUG:表示不遍歷調(diào)試內(nèi)容,即跳過源文件,源碼調(diào)試擴展,局部變量表,局部變量類型表和行號表屬性,即以下方法既不會被解析也不會被訪問(ClassVisitor.visitSource,MethodVisitor.visitLocalVariable,MethodVisitor.visitLineNumber)。使用此標識后,類文件調(diào)試信息會被去除,請警記。 -
ClassReader.SKIP_CODE:設(shè)置該標識,則代碼屬性將不會被轉(zhuǎn)換和訪問,例如方法體代碼不會進行解析和訪問。 -
ClassReader.SKIP_FRAMES:設(shè)置該標識,表示跳過棧圖(StackMap)和棧圖表(StackMapTable)屬性,即MethodVisitor.visitFrame方法不會被轉(zhuǎn)換和訪問。當設(shè)置了ClassWriter.COMPUTE_FRAMES時,設(shè)置該標識會很有用,因為他避免了訪問幀內(nèi)容(這些內(nèi)容會被忽略和重新計算,無需訪問)。 -
ClassReader.EXPAND_FRAMES:該標識用于設(shè)置擴展棧幀圖。默認棧圖以它們原始格式(V1_6以下使用擴展格式,其他使用壓縮格式)被訪問。如果設(shè)置該標識,棧圖則始終以擴展格式進行訪問(此標識在ClassReader和ClassWriter中增加了解壓/壓縮步驟,會大幅度降低性能)。
ClassWriter

這個類是ClassVisitor的一個實現(xiàn)類,這個類中的toByteArray方法會將最終修改的字節(jié)碼以 byte 數(shù)組形式返回。它可以單獨使用,也可以傳遞給一個或多個ClassReader或ClassVisitor適配器修改一個或多個已存在的Java類的類文件。
我們知道,類文件有著自己嚴格的格式,當我們想要注入相關(guān)代碼時,不是直接注入相關(guān)指令就可以的,比如對于方法注入,我們可能還需要對棧幀圖( stack map frames)進行計算:你需要計算所有的幀,找到有對象跳轉(zhuǎn)或者絕對跳轉(zhuǎn)的幀,最后還要壓縮剩余的幀。同樣,對于棧幀的局部變量表和操作數(shù)棧的大小也要自己進行計算。這些計算操作具備一定的難度,幸運的是,當我們創(chuàng)建一個ClassWriter時,可以配置 ASM 自動幫我們對指定的內(nèi)容進行計算。具體的配置標識如下:
ClassWriter的構(gòu)造函數(shù)需要傳入一個 flag,其含義為:
-
ClassWriter(0):表示 ASM 不會自動自動幫你計算棧幀和局部變量表和操作數(shù)棧大小。 -
ClassWriter(ClassWriter.COMPUTE_MAXS):表示 ASM 會自動幫你計算局部變量表和操作數(shù)棧的大小,但是你還是需要調(diào)用visitMaxs方法,但是可以使用任意參數(shù),因為它們會被忽略。帶有這個標識,對于棧幀大小,還是需要你手動計算。 -
ClassWriter(ClassWriter.COMPUTE_FRAMES):表示 ASM 會自動幫你計算所有的內(nèi)容。你不必去調(diào)用visitFrame,但是你還是需要調(diào)用visitMaxs方法(參數(shù)可任意設(shè)置,同樣會被忽略)。
使用這些標識很方便,但是會帶來一些性能上的損失:COMPUTE_MAXS標識會使ClassWriter慢10%,COMPUTE_FRAMES標識會使ClassWriter慢2倍,
ClassVisitor

一個可以訪問Java類的訪問者。其方法被調(diào)用次序必須滿足:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd
即visit必須第一個被調(diào)用,然后最多調(diào)用一次visitSource,同樣接著最多調(diào)用一次visitOuterClass,接下來按任意順序可多次調(diào)用;最后調(diào)用一次visitAnnotation和visitAttribute;接下來visitInnerClass,visitField,visitMethod同樣按任意順序可多次調(diào)用visitEnd,表示類訪問結(jié)束。
注: ASM 文檔原文內(nèi)容為:
This means that visit must be called ?rst, followed by at most one call to visitSource, followed by at most one call to visitOuterClass, followed by any number of calls in any order to visitAnnotation and visitAttribute, followed by any number of calls in any order to visitInnerClass,visitField and visitMethod , and terminated by a single call to visitEnd.
黑體加粗句子我的翻譯是:以任意順序訪問visitInnerClass,visitField和visitMethod,但是在我機器上試驗得到的結(jié)果是這3者的訪問順序是固定的:visitInnerClass->visitField->visitMethod,所以,此處可能是翻譯有問題,應(yīng)該是以一定的順序可多次調(diào)用 visitInnerClass,visitField 和visitMethod。如有差錯,煩請指正,感謝! ^-^
MethodVisitor

ASM 生成和轉(zhuǎn)換class文件方法使用的是抽象類MethodVisitor,ClassVisitor.visitMethod方法返回的就是該實例。
其方法調(diào)用時序為:
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
即如果有annotations或者attributes,它們必須被第一個訪問,接下來對于非抽象方法訪問的就是方法內(nèi)部字節(jié)碼(visitCode),然后在visitCode和visitMaxs中的那些指令會按上面所示方法順序訪問,最后類方法訪問結(jié)束回調(diào)visitEnd。
在class文件中,方法中的代碼是以一系列的字節(jié)碼指令組成的。如果要生成或者改變類內(nèi)容,則需要先了解下這些指令的工作模型。
下面簡單介紹指令的工作模型,了解這些內(nèi)容就基本能夠完成對類的一些簡單的變換操作。如需更詳細介紹,請參考 JVM 規(guī)范。
JVM 執(zhí)行模型
在介紹字節(jié)碼指令之前,有必要先介紹下 JVM 的執(zhí)行模型。我們都知道,Java 代碼是運行在線程中的,每條線程都擁有屬于自己的運行棧,棧是由一個或多個幀組成的,也叫棧幀(StackFrame)。每個棧幀代表一個方法調(diào)用:每當線程調(diào)用一個Java方法時,JVM就會在該線程對應(yīng)的棧中壓入一個幀;當執(zhí)行這個方法時,它使用這個幀來存儲參數(shù)、局部變量、中間運算結(jié)果等等;當方法執(zhí)行結(jié)束(無論是正常返回還是拋異常)時,該棧幀就會彈出,然后繼續(xù)運行下一個棧幀(棧頂棧幀)的方法調(diào)用。棧幀
棧幀由三部分組成:局部變量表、操作數(shù)棧、幀數(shù)據(jù)區(qū)。
局部變量表 被組織為以一個字長(32 bit)為單位、從0開始計數(shù)的數(shù)組,類型為short、byte和char的值在存入數(shù)組前要被轉(zhuǎn)換成int值,而long和double在數(shù)組中占據(jù)連續(xù)的兩項,在訪問局部變量中的long或double時,只需取出連續(xù)兩項的第一項的索引值即可,如某個long值在局部變量 區(qū)中占據(jù)的索引時3、4項,取值時,指令只需取索引為3的long值即可。
操作數(shù)棧 和局部變量表一樣,操作數(shù)棧也被組織成一個以字長為單位的數(shù)組。但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的??砂巡僮鲾?shù)棧理解為存儲計算時,臨時數(shù)據(jù)的存儲區(qū)域。
幀數(shù)據(jù)區(qū) 幀數(shù)據(jù)區(qū)除了局部變量表和操作數(shù)棧外,Java棧幀還需要一些數(shù)據(jù)來支持常量池解析、正常方法返回以及異常派發(fā)機制。這些數(shù)據(jù)都保存在Java棧幀的幀數(shù)據(jù)區(qū)中。
當JVM執(zhí)行到需要常量池數(shù)據(jù)的指令時,它都會通過幀數(shù)據(jù)區(qū)中指向常量池的指針來訪問它。
除了處理常量池解析外,幀里的數(shù)據(jù)還要處理Java方法的正常結(jié)束和異常終止。如果是通過return正常結(jié)束,則當前棧幀從Java棧中彈出,恢復(fù)發(fā)起調(diào)用的方法的棧。如果方法有返回值,JVM會把返回值壓入到發(fā)起調(diào)用方法的操作數(shù)棧。
為了處理Java方法中的異常情況,幀數(shù)據(jù)區(qū)還必須保存一個對此方法異常引用表的引用。當異常拋出時,JVM給catch塊中的代碼。如果沒發(fā)現(xiàn),方法立即終止,然后JVM用幀區(qū)數(shù)據(jù)的信息恢復(fù)發(fā)起調(diào)用的方法的幀。然后再發(fā)起調(diào)用方法的上下文重新拋出同樣的異常。
局部變量表和操作數(shù)棧的大小決于方法代碼,它們在編譯時進行計算,并與類中的字節(jié)碼指令一起存儲。因此,對于同一個方法調(diào)用,所有幀的大小都是一樣的,但是對于不同的方法調(diào)用,各個棧幀都擁有不同大小的局部變量表和操作數(shù)棧。

表 3.1 展示一個帶有3個幀的運行棧樣例。第一個幀包含3個局部變量,其操作數(shù)棧為4個字長大小,包含2個值。第二個幀包含2個局部變量和2個操作數(shù)值。第三個幀處于棧頂(當前幀),包含4個局部變量和2個操作數(shù)值。
當空棧壓入一個幀時,其局部變量表會被初始化壓入目標對象實例this(對于非靜態(tài)方法)和方法參數(shù)變量。比如,調(diào)用a.equals(b)時,會創(chuàng)建一個幀,其局部變量表初始化有2個局部變量a和b(其他變量為被初始化)。
字節(jié)碼指令
參考 Jvm系列2—字節(jié)碼指令
在基于堆棧的的虛擬機中,指令的主戰(zhàn)場便是操作數(shù)棧,除了load是從局部變量表加載數(shù)據(jù)到操作數(shù)棧以及store儲存數(shù)據(jù)到局部變量表,其余指令基本都是用于操作數(shù)棧的。示例
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}
上面代碼的getF方法的字節(jié)碼如下:
ALOAD 0
GETFIELD pkg/Bean f I
IRETURN
第一條指令讀取局部變量表索引0的局部變量this,并將值壓入到操作數(shù)棧中。第二條指令先獲取操作數(shù)棧棧頂值this(彈出棧),獲取該實例類成員f,并將其壓入棧中。最后一條指令將操作數(shù)棧彈出,將值返回給調(diào)用者。具體過程如下圖3.2 所示:

上面代碼的setF(int f)方法的字節(jié)碼如下:
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
RETURN
第一條指令將局部變量表索引0的變量this壓入到操作數(shù)棧;第二條指令將局部變量表索引1的變量f壓入到操作數(shù)棧;第三條指令彈出這個值,并且將一個int值付給this.f;最后一條指令將當前棧幀銷毀并將結(jié)果返回給調(diào)用者。具體過程如下圖3.3 所示:

AnnotationVisitor

AnnotationVisitor api 訪問時序如下:
( visit | visitEnum | visitAnnotation | visitArray )* visitEnd
Type

Type對應(yīng)的是 Java 類型,該類提供一些方法方便我們操控 Java 類型和描述符轉(zhuǎn)換。
比如:
-
Type.INT_TYPE表示一個int類型的Type實例。 -
Class -> Type:
Type.getType(String.class)會返回String對應(yīng)的Type類型。 -
Descriptor -> Type:
Type.getType("Ljava/lang/String;)會返回類型描述符對應(yīng)的Type類型。 -
InternalName -> Type:
Type.getObjectType("java/lang/String")會返回參數(shù) InternalName 對應(yīng)的Type類型。 -
Type -> ClassName:
Type.getType(String.class).getClassName()會返回java.lang.String。 -
Type -> InternalName:
Type.getType(String.class).getInternalName()會返回String.class的 InternalName,即java/lang/String,該方法只適用于class類型或者interface類型。 -
Type -> Descriptor:
Type.getType(String.class).getDescriptor()會返回String.class的 Descriptor,即Ljava/lang/String; -
MethodDescriptor -> ArgumentType:
Type.getArgumentTypes("(I)V")會返回方法描述符對應(yīng)的參數(shù)Type[]數(shù)組,比如此處返回的是{Type.INT_TYPE}。 -
MethodDescriptor -> ReturnType:
Type.getReturnType("(I)V")會返回方法描述符對應(yīng)的函數(shù)返回值Type類型,比如此處返回的是Type.VOID_TYPE。
Notice
- 通常我們綁定
ClassVisitor到ClassReader的代碼如下:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
假設(shè)我們并不想做出改動類本身行為,那么按照上面的代碼,效率會比較低,因為必須解析字節(jié)數(shù)組并且要經(jīng)歷事件循環(huán);如果可以直接復(fù)制原本的字節(jié)數(shù)組b1到b2,那么效率將大大提升。幸運的是,ASM 已考慮到這種情況,并為我們提供了優(yōu)化方法,如下所示:
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0); //pass cr to cw directly
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
原理如下:
- 如果
ClassReader組件檢測到作為其accept參數(shù)的ClassVisitor返回的MethodVisitor是來自ClassWriter的,這表明該方法沒有被改動,事實上甚至不會被程序感知。 - 這種情況下,
ClassReader組件就不去解析該方法內(nèi)容,并且不會產(chǎn)生相應(yīng)事件,只是從ClassWriter復(fù)制這部分方法的字節(jié)碼數(shù)組。
使用優(yōu)化方法,性能上比前者提升有2倍多速度。需要注意的是,這種優(yōu)化方法需要復(fù)制類中所有常量到新的字節(jié)數(shù)組中,這種優(yōu)化對于增加成員,方法和指令來說,是沒有問題的,但是對于刪除或者更改類元素名稱來說,會大大增加類文件大小,因此,建議對于 增加 動作的轉(zhuǎn)換使用優(yōu)化方法。