前言
前面一篇文章 ASM 簡(jiǎn)介 對(duì) ASM 框架做了簡(jiǎn)單的介紹惊搏。
本篇文章主要對(duì)該框架的 Core Api 其中重要的一些類進(jìn)行詳細(xì)的介紹,讓大家可以更得心應(yīng)手的使用 ASM饼暑。
在開(kāi)始之前卸奉,讓我們先回顧一下 ASM Core Api 調(diào)用流程:
ASM 提供了一個(gè)類
ClassReader
可以方便地讓我們對(duì)class
文件進(jìn)行讀取與解析盐类;ASM 在
ClassReader
解析class
文件過(guò)程中拭荤,解析到某一個(gè)結(jié)構(gòu)就會(huì)通知到ClassVisitor
的相應(yīng)方法(eg:解析到類方法時(shí)插勤,就會(huì)回調(diào)ClassVisitor.visitMethod
方法)谍肤;可以通過(guò)更改
ClassVisitor
中相應(yīng)結(jié)構(gòu)方法返回值琅关,實(shí)現(xiàn)對(duì)類的代碼切入(eg:更改ClassVisitor.visitMethod()
方法的默認(rèn)返回值MethodVisitor
實(shí)例煮岁,通過(guò)操作該自定義MethodVisitor
從而實(shí)現(xiàn)對(duì)原方法的改寫(xiě));其它的結(jié)構(gòu)遍歷也如同
ClassVisitor
涣易;通過(guò)
ClassWriter
的toByteArray()
方法画机,得到class
文件的字節(jié)碼內(nèi)容,最后通過(guò)文件流寫(xiě)入方式覆蓋掉原先的內(nèi)容新症,實(shí)現(xiàn)class
文件的改寫(xiě)步氏。
以上,就是 ASM Core Api 的整體運(yùn)作流程账劲。
接下來(lái)戳护,我將對(duì)其中涉及到的重要的類進(jìn)行詳細(xì)解析。
ClassReader
這個(gè)類會(huì)提供你要轉(zhuǎn)變的類的字節(jié)數(shù)組瀑焦,它的accept
方法腌且,接受一個(gè)具體的ClassVisitor
,并調(diào)用實(shí)現(xiàn)中具體的 visit,
visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass,visitField, visitMethod和 visitEnd 方法榛瓮。
ClassReader.accept(ClassVisitor classVisitor, int parsingOptions)
中铺董,第二個(gè)參數(shù)parsingOptions
的取值有以下選項(xiàng):
-
ClassReader.SKIP_DEBUG
:表示不遍歷調(diào)試內(nèi)容,即跳過(guò)源文件,源碼調(diào)試擴(kuò)展精续,局部變量表坝锰,局部變量類型表和行號(hào)表屬性,即以下方法既不會(huì)被解析也不會(huì)被訪問(wèn)(ClassVisitor.visitSource
重付,MethodVisitor.visitLocalVariable
顷级,MethodVisitor.visitLineNumber
)。使用此標(biāo)識(shí)后确垫,類文件調(diào)試信息會(huì)被去除弓颈,請(qǐng)警記。 -
ClassReader.SKIP_CODE
:設(shè)置該標(biāo)識(shí)删掀,則代碼屬性將不會(huì)被轉(zhuǎn)換和訪問(wèn)翔冀,例如方法體代碼不會(huì)進(jìn)行解析和訪問(wèn)。 -
ClassReader.SKIP_FRAMES
:設(shè)置該標(biāo)識(shí)披泪,表示跳過(guò)棧圖(StackMap)和棧圖表(StackMapTable)屬性纤子,即MethodVisitor.visitFrame
方法不會(huì)被轉(zhuǎn)換和訪問(wèn)。當(dāng)設(shè)置了ClassWriter.COMPUTE_FRAMES
時(shí)款票,設(shè)置該標(biāo)識(shí)會(huì)很有用控硼,因?yàn)樗苊饬嗽L問(wèn)幀內(nèi)容(這些內(nèi)容會(huì)被忽略和重新計(jì)算,無(wú)需訪問(wèn))徽职。 -
ClassReader.EXPAND_FRAMES
:該標(biāo)識(shí)用于設(shè)置擴(kuò)展棧幀圖象颖。默認(rèn)棧圖以它們?cè)几袷剑╒1_6以下使用擴(kuò)展格式佩厚,其他使用壓縮格式)被訪問(wèn)姆钉。如果設(shè)置該標(biāo)識(shí),棧圖則始終以擴(kuò)展格式進(jìn)行訪問(wèn)(此標(biāo)識(shí)在ClassReader
和ClassWriter
中增加了解壓/壓縮步驟抄瓦,會(huì)大幅度降低性能)潮瓶。
ClassWriter
這個(gè)類是ClassVisitor
的一個(gè)實(shí)現(xiàn)類,這個(gè)類中的toByteArray
方法會(huì)將最終修改的字節(jié)碼以 byte 數(shù)組形式返回钙姊。它可以單獨(dú)使用毯辅,也可以傳遞給一個(gè)或多個(gè)ClassReader
或ClassVisitor
適配器修改一個(gè)或多個(gè)已存在的Java類的類文件。
我們知道煞额,類文件有著自己嚴(yán)格的格式思恐,當(dāng)我們想要注入相關(guān)代碼時(shí),不是直接注入相關(guān)指令就可以的膊毁,比如對(duì)于方法注入胀莹,我們可能還需要對(duì)棧幀圖( stack map frames)進(jìn)行計(jì)算:你需要計(jì)算所有的幀,找到有對(duì)象跳轉(zhuǎn)或者絕對(duì)跳轉(zhuǎn)的幀婚温,最后還要壓縮剩余的幀描焰。同樣,對(duì)于棧幀的局部變量表和操作數(shù)棧的大小也要自己進(jìn)行計(jì)算栅螟。這些計(jì)算操作具備一定的難度荆秦,幸運(yùn)的是篱竭,當(dāng)我們創(chuàng)建一個(gè)ClassWriter
時(shí),可以配置 ASM 自動(dòng)幫我們對(duì)指定的內(nèi)容進(jìn)行計(jì)算步绸。具體的配置標(biāo)識(shí)如下:
ClassWriter
的構(gòu)造函數(shù)需要傳入一個(gè) flag掺逼,其含義為:
-
ClassWriter(0)
:表示 ASM 不會(huì)自動(dòng)自動(dòng)幫你計(jì)算棧幀和局部變量表和操作數(shù)棧大小。 -
ClassWriter(ClassWriter.COMPUTE_MAXS)
:表示 ASM 會(huì)自動(dòng)幫你計(jì)算局部變量表和操作數(shù)棧的大小坪圾,但是你還是需要調(diào)用visitMaxs
方法,但是可以使用任意參數(shù)惑朦,因?yàn)樗鼈儠?huì)被忽略兽泄。帶有這個(gè)標(biāo)識(shí),對(duì)于棧幀大小漾月,還是需要你手動(dòng)計(jì)算病梢。 -
ClassWriter(ClassWriter.COMPUTE_FRAMES)
:表示 ASM 會(huì)自動(dòng)幫你計(jì)算所有的內(nèi)容。你不必去調(diào)用visitFrame
梁肿,但是你還是需要調(diào)用visitMaxs
方法(參數(shù)可任意設(shè)置蜓陌,同樣會(huì)被忽略)。
使用這些標(biāo)識(shí)很方便吩蔑,但是會(huì)帶來(lái)一些性能上的損失:COMPUTE_MAXS
標(biāo)識(shí)會(huì)使ClassWriter
慢10%钮热,COMPUTE_FRAMES
標(biāo)識(shí)會(huì)使ClassWriter
慢2倍,
ClassVisitor
一個(gè)可以訪問(wèn)Java
類的訪問(wèn)者烛芬。其方法被調(diào)用次序必須滿足:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd
即visit
必須第一個(gè)被調(diào)用隧期,然后最多調(diào)用一次visitSource
,同樣接著最多調(diào)用一次visitOuterClass
赘娄,接下來(lái)按任意順序可多次調(diào)用性置;最后調(diào)用一次visitAnnotation
和visitAttribute
仆潮;接下來(lái)visitInnerClass
,visitField
遣臼,visitMethod
同樣按任意順序可多次調(diào)用visitEnd
,表示類訪問(wèn)結(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.
黑體加粗句子我的翻譯是:以任意順序訪問(wèn)visitInnerClass
,visitField
和visitMethod
鹏浅,但是在我機(jī)器上試驗(yàn)得到的結(jié)果是這3者的訪問(wèn)順序是固定的:visitInnerClass
->visitField
->visitMethod
,所以屏歹,此處可能是翻譯有問(wèn)題隐砸,應(yīng)該是以一定的順序可多次調(diào)用 visitInnerClass
,visitField
和visitMethod
。如有差錯(cuò)西采,煩請(qǐng)指正凰萨,感謝! ^-^
MethodVisitor
ASM 生成和轉(zhuǎn)換class
文件方法使用的是抽象類MethodVisitor
,ClassVisitor.visitMethod
方法返回的就是該實(shí)例胖眷。
其方法調(diào)用時(shí)序?yàn)椋?/p>
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
即如果有annotations
或者attributes
武通,它們必須被第一個(gè)訪問(wèn),接下來(lái)對(duì)于非抽象方法訪問(wèn)的就是方法內(nèi)部字節(jié)碼(visitCode
)珊搀,然后在visitCode
和visitMaxs
中的那些指令會(huì)按上面所示方法順序訪問(wèn)冶忱,最后類方法訪問(wèn)結(jié)束回調(diào)visitEnd
。
在class
文件中境析,方法中的代碼是以一系列的字節(jié)碼指令組成的囚枪。如果要生成或者改變類內(nèi)容,則需要先了解下這些指令的工作模型劳淆。
下面簡(jiǎn)單介紹指令的工作模型链沼,了解這些內(nèi)容就基本能夠完成對(duì)類的一些簡(jiǎn)單的變換操作。如需更詳細(xì)介紹沛鸵,請(qǐng)參考 JVM 規(guī)范括勺。
JVM 執(zhí)行模型
在介紹字節(jié)碼指令之前,有必要先介紹下 JVM 的執(zhí)行模型曲掰。我們都知道疾捍,Java 代碼是運(yùn)行在線程中的栏妖,每條線程都擁有屬于自己的運(yùn)行棧乱豆,棧是由一個(gè)或多個(gè)幀組成的,也叫棧幀(StackFrame)宛裕。每個(gè)棧幀代表一個(gè)方法調(diào)用:每當(dāng)線程調(diào)用一個(gè)Java方法時(shí)孵奶,JVM就會(huì)在該線程對(duì)應(yīng)的棧中壓入一個(gè)幀;當(dāng)執(zhí)行這個(gè)方法時(shí)油航,它使用這個(gè)幀來(lái)存儲(chǔ)參數(shù)、局部變量沙合、中間運(yùn)算結(jié)果等等首懈;當(dāng)方法執(zhí)行結(jié)束(無(wú)論是正常返回還是拋異常)時(shí),該棧幀就會(huì)彈出盯仪,然后繼續(xù)運(yùn)行下一個(gè)棧幀(棧頂棧幀)的方法調(diào)用。棧幀
棧幀由三部分組成:局部變量表揭鳞、操作數(shù)棧乓梨、幀數(shù)據(jù)區(qū)昆雀。
局部變量表 被組織為以一個(gè)字長(zhǎng)(32 bit)為單位揩懒、從0開(kāi)始計(jì)數(shù)的數(shù)組渠缕,類型為short
、byte
和char
的值在存入數(shù)組前要被轉(zhuǎn)換成int
值测暗,而long
和double
在數(shù)組中占據(jù)連續(xù)的兩項(xiàng)稚字,在訪問(wèn)局部變量中的long
或double
時(shí)袄友,只需取出連續(xù)兩項(xiàng)的第一項(xiàng)的索引值即可,如某個(gè)long
值在局部變量 區(qū)中占據(jù)的索引時(shí)3、4項(xiàng),取值時(shí),指令只需取索引為3的long
值即可纱耻。
操作數(shù)棧 和局部變量表一樣镐躲,操作數(shù)棧也被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組入录。但和前者不同的是分预,它不是通過(guò)索引來(lái)訪問(wèn)的配乓,而是通過(guò)入棧和出棧來(lái)訪問(wèn)的。可把操作數(shù)棧理解為存儲(chǔ)計(jì)算時(shí),臨時(shí)數(shù)據(jù)的存儲(chǔ)區(qū)域蝗锥。
幀數(shù)據(jù)區(qū) 幀數(shù)據(jù)區(qū)除了局部變量表和操作數(shù)棧外跃洛,Java棧幀還需要一些數(shù)據(jù)來(lái)支持常量池解析、正常方法返回以及異常派發(fā)機(jī)制终议。這些數(shù)據(jù)都保存在Java棧幀的幀數(shù)據(jù)區(qū)中汇竭。
當(dāng)JVM執(zhí)行到需要常量池?cái)?shù)據(jù)的指令時(shí),它都會(huì)通過(guò)幀數(shù)據(jù)區(qū)中指向常量池的指針來(lái)訪問(wèn)它穴张。
除了處理常量池解析外细燎,幀里的數(shù)據(jù)還要處理Java方法的正常結(jié)束和異常終止。如果是通過(guò)return正常結(jié)束皂甘,則當(dāng)前棧幀從Java棧中彈出玻驻,恢復(fù)發(fā)起調(diào)用的方法的棧。如果方法有返回值偿枕,JVM會(huì)把返回值壓入到發(fā)起調(diào)用方法的操作數(shù)棧璧瞬。
為了處理Java方法中的異常情況,幀數(shù)據(jù)區(qū)還必須保存一個(gè)對(duì)此方法異常引用表的引用益老。當(dāng)異常拋出時(shí)彪蓬,JVM給catch塊中的代碼。如果沒(méi)發(fā)現(xiàn)捺萌,方法立即終止档冬,然后JVM用幀區(qū)數(shù)據(jù)的信息恢復(fù)發(fā)起調(diào)用的方法的幀。然后再發(fā)起調(diào)用方法的上下文重新拋出同樣的異常桃纯。
局部變量表和操作數(shù)棧的大小決于方法代碼酷誓,它們?cè)诰幾g時(shí)進(jìn)行計(jì)算,并與類中的字節(jié)碼指令一起存儲(chǔ)态坦。因此盐数,對(duì)于同一個(gè)方法調(diào)用,所有幀的大小都是一樣的伞梯,但是對(duì)于不同的方法調(diào)用玫氢,各個(gè)棧幀都擁有不同大小的局部變量表和操作數(shù)棧。
表 3.1 展示一個(gè)帶有3個(gè)幀的運(yùn)行棧樣例谜诫。第一個(gè)幀包含3個(gè)局部變量漾峡,其操作數(shù)棧為4個(gè)字長(zhǎng)大小,包含2個(gè)值喻旷。第二個(gè)幀包含2個(gè)局部變量和2個(gè)操作數(shù)值生逸。第三個(gè)幀處于棧頂(當(dāng)前幀),包含4個(gè)局部變量和2個(gè)操作數(shù)值。
當(dāng)空棧壓入一個(gè)幀時(shí)槽袄,其局部變量表會(huì)被初始化壓入目標(biāo)對(duì)象實(shí)例this
(對(duì)于非靜態(tài)方法)和方法參數(shù)變量烙无。比如,調(diào)用a.equals(b)
時(shí)遍尺,會(huì)創(chuàng)建一個(gè)幀截酷,其局部變量表初始化有2個(gè)局部變量a
和b
(其他變量為被初始化)。
字節(jié)碼指令
參考 Jvm系列2—字節(jié)碼指令
在基于堆棧的的虛擬機(jī)中狮鸭,指令的主戰(zhàn)場(chǎng)便是操作數(shù)棧合搅,除了load是從局部變量表加載數(shù)據(jù)到操作數(shù)棧以及store儲(chǔ)存數(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
(彈出棧)惯退,獲取該實(shí)例類成員f
,并將其壓入棧中从藤。最后一條指令將操作數(shù)棧彈出催跪,將值返回給調(diào)用者。具體過(guò)程如下圖3.2 所示:
上面代碼的setF(int f)
方法的字節(jié)碼如下:
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
RETURN
第一條指令將局部變量表索引0的變量this
壓入到操作數(shù)棧夷野;第二條指令將局部變量表索引1的變量f
壓入到操作數(shù)棧懊蒸;第三條指令彈出這個(gè)值,并且將一個(gè)int
值付給this.f
悯搔;最后一條指令將當(dāng)前棧幀銷毀并將結(jié)果返回給調(diào)用者骑丸。具體過(guò)程如下圖3.3 所示:
AnnotationVisitor
AnnotationVisitor
api 訪問(wèn)時(shí)序如下:
( visit | visitEnum | visitAnnotation | visitArray )* visitEnd
Type
Type
對(duì)應(yīng)的是 Java 類型,該類提供一些方法方便我們操控 Java 類型和描述符轉(zhuǎn)換妒貌。
比如:
-
Type.INT_TYPE
表示一個(gè)int
類型的Type
實(shí)例通危。 -
Class -> Type:
Type.getType(String.class)
會(huì)返回String
對(duì)應(yīng)的Type
類型。 -
Descriptor -> Type:
Type.getType("Ljava/lang/String;)
會(huì)返回類型描述符對(duì)應(yīng)的Type
類型灌曙。 -
InternalName -> Type:
Type.getObjectType("java/lang/String")
會(huì)返回參數(shù) InternalName 對(duì)應(yīng)的Type
類型菊碟。 -
Type -> ClassName:
Type.getType(String.class).getClassName()
會(huì)返回java.lang.String
。 -
Type -> InternalName:
Type.getType(String.class).getInternalName()
會(huì)返回String.class
的 InternalName在刺,即java/lang/String
逆害,該方法只適用于class
類型或者interface
類型。 -
Type -> Descriptor:
Type.getType(String.class).getDescriptor()
會(huì)返回String.class
的 Descriptor蚣驼,即Ljava/lang/String;
-
MethodDescriptor -> ArgumentType:
Type.getArgumentTypes("(I)V")
會(huì)返回方法描述符對(duì)應(yīng)的參數(shù)Type[]
數(shù)組魄幕,比如此處返回的是{Type.INT_TYPE}
。 -
MethodDescriptor -> ReturnType:
Type.getReturnType("(I)V")
會(huì)返回方法描述符對(duì)應(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è)我們并不想做出改動(dòng)類本身行為,那么按照上面的代碼,效率會(huì)比較低队丝,因?yàn)楸仨毥馕鲎止?jié)數(shù)組并且要經(jīng)歷事件循環(huán)靡馁;如果可以直接復(fù)制原本的字節(jié)數(shù)組b1
到b2
,那么效率將大大提升机久。幸運(yùn)的是臭墨,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
組件檢測(cè)到作為其accept
參數(shù)的ClassVisitor
返回的MethodVisitor
是來(lái)自ClassWriter
的胧弛,這表明該方法沒(méi)有被改動(dòng),事實(shí)上甚至不會(huì)被程序感知侠畔。 - 這種情況下结缚,
ClassReader
組件就不去解析該方法內(nèi)容,并且不會(huì)產(chǎn)生相應(yīng)事件软棺,只是從ClassWriter
復(fù)制這部分方法的字節(jié)碼數(shù)組红竭。
使用優(yōu)化方法,性能上比前者提升有2倍多速度喘落。需要注意的是茵宪,這種優(yōu)化方法需要復(fù)制類中所有常量到新的字節(jié)數(shù)組中,這種優(yōu)化對(duì)于增加成員瘦棋,方法和指令來(lái)說(shuō)稀火,是沒(méi)有問(wèn)題的,但是對(duì)于刪除或者更改類元素名稱來(lái)說(shuō)赌朋,會(huì)大大增加類文件大小凰狞,因此,建議對(duì)于 增加 動(dòng)作的轉(zhuǎn)換使用優(yōu)化方法箕慧。