Java 字節(jié)碼操作框架——ASM
今天我們將介紹字節(jié)碼相關(guān)的應(yīng)用場(chǎng)景,首先要介紹的是如何對(duì)字節(jié)碼做解析和修改,本文將會(huì)詳細(xì)給大家介紹一個(gè)工業(yè)級(jí)字節(jié)碼操作框架 ASM。
ASM
當(dāng)我們需要對(duì)一個(gè) class 文件做修改時(shí),我們可以選擇自己解析這個(gè)class 文件,在符合 Java 字節(jié)碼規(guī)范的前提下進(jìn)行字節(jié)碼改造。如果你寫(xiě)過(guò) class 文件的解析程序,會(huì)發(fā)現(xiàn)這個(gè)過(guò)程極其繁瑣,更別說(shuō)進(jìn)行增加方法等操作了。
ASM 最開(kāi)始是 2000 年 Eric Bruneton 在 INRIA(法國(guó)國(guó)立計(jì)算機(jī)及自動(dòng)化研究院)讀博士期間完成的一個(gè)作品。那個(gè)時(shí)候包含 java.lang.reflect.Proxy 包的 JDK 1.3 還沒(méi)發(fā)布,ASM 被作為代碼生成器,用來(lái)生成動(dòng)態(tài)代理的代理類(lèi)。經(jīng)過(guò)多年的發(fā)展,ASM 在諸多框架中已經(jīng)遍地開(kāi)花,成為字節(jié)碼操作領(lǐng)域事實(shí)上的標(biāo)準(zhǔn)。
簡(jiǎn)單的 API 背后 ASM 自動(dòng)幫我們做了很多事情,比如維護(hù)常量池的索引,計(jì)算最大棧大小 max_stack,局部變量表大小 max_locals 等,除此之外還有下面這些優(yōu)點(diǎn):
架構(gòu)設(shè)計(jì)精巧,使用方便。
更新速度快,支持最新的 Java 版本
速度非???,在動(dòng)態(tài)代理 class 的生成和 class 的轉(zhuǎn)換時(shí),盡可能確保運(yùn)行中的應(yīng)用不會(huì)被 ASM 拖慢
非??煽?、久經(jīng)考驗(yàn),已經(jīng)有很多著名的開(kāi)源框架都在使用,例如 cglib,、mybatis、fastjson 其它字節(jié)碼操作框架在操作字節(jié)碼的過(guò)程中生成大量的中間類(lèi)和對(duì)象,耗費(fèi)大量的內(nèi)存且運(yùn)行緩慢,ASM 使用了訪問(wèn)者(Visitor)設(shè)計(jì)模式,避免了創(chuàng)建和消耗大量的中間變量。
ASM 提供了兩種生成和轉(zhuǎn)換類(lèi)的方法: 基于事件觸發(fā)的 core API 和基于對(duì)象的 Tree API,這兩種方式可以用 XML 解析的 SAX 和 DOM 方式來(lái)對(duì)照。
SAX 解析 XML 文件采用的是事件驅(qū)動(dòng),它不需要解析完整個(gè)文檔,而是一邊按內(nèi)容順序解析文檔,如果解析時(shí)符合特定的事件則回調(diào)一些函數(shù)來(lái)處理事件。SAX運(yùn)行時(shí)是單向的、流式的,解析過(guò)的部分無(wú)法在不重新開(kāi)始的情況下再次讀取,ASM 的 Core API 類(lèi)似于這種方式。
DOM 解析方式則會(huì)將整個(gè) XML 作為類(lèi)似樹(shù)結(jié)構(gòu)的方式讀入內(nèi)存中以便操作及解析,ASM 的 Tree API 類(lèi)似于這種方式。以下面的 XML 文件為例:
對(duì)應(yīng)的 SAX 和 DOM 解析方式的如下圖所示:

ASM 核心類(lèi)介紹

ClassReader
它是字節(jié)碼讀取和分析引擎,幫我們做了最苦最累的解析二進(jìn)制的 class 文件字節(jié)碼的活。采用類(lèi)似于 SAX 的事件讀取機(jī)制,每當(dāng)有事件發(fā)生時(shí),觸發(fā)相應(yīng)的 ClassVisitor、MethodVisitor 等做相應(yīng)的處理。
ClassVisitor
它是一個(gè)抽象類(lèi),ClassReader 對(duì)象創(chuàng)建之后,調(diào)用 ClassReader.accept() 方法,傳入一個(gè) ClassVisitor 對(duì)象。ClassVisitor 在解析字節(jié)碼的過(guò)程中遇到不同的節(jié)點(diǎn)時(shí)會(huì)調(diào)用不同的 visit() 方法,比如 visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass, visitField, visitMethod 和 visitEnd方法。在上述 visit 的過(guò)程中還會(huì)產(chǎn)生一些子過(guò)程,比如 visitAnnotation 會(huì)觸發(fā) AnnotationVisitor 的調(diào)用、visitMethod 會(huì)觸發(fā) MethodVisitor 的調(diào)用。正是在這些 visit 的過(guò)程中,我們得以有機(jī)會(huì)去修改各個(gè)子節(jié)點(diǎn)的字節(jié)碼。
ClassVisitor 類(lèi)中的 visit 方法必須按照以下的順序被調(diào)用執(zhí)行:
visit 方法最先被調(diào)用,接著調(diào)用零次或一次 visitSource 方法,接著調(diào)用零次或一次 visitOuterClass 方法,再接下來(lái)按任意順序調(diào)用任意多次 visitAnnotation 和 visitAttribute 方法,再接下來(lái)按任意順序調(diào)用任意多次 visitInnerClass、visitField、visitMethod 方法,visitEnd 最后被調(diào)用。

ClassWriter
這個(gè)類(lèi)是 ClassVisitor 抽象類(lèi)的一個(gè)實(shí)現(xiàn)類(lèi),其之前的每個(gè) ClassVisitor 都可能對(duì)原始的字節(jié)碼做修改,ClassWriter 的 toByteArray 方法則把最終修改的字節(jié)碼以 byte 數(shù)組的形式返回
這三個(gè)核心類(lèi)的關(guān)系如下圖

一個(gè)最簡(jiǎn)單的用法如下面的代碼所示:
上面的代碼中,ClassReader 負(fù)責(zé)讀取類(lèi)文件字節(jié)數(shù)組,accept 調(diào)用之后 ClassReader 會(huì)把解析字節(jié)碼過(guò)程的事件源源不斷的通知給 ClassVisitor 對(duì)象調(diào)用不同的 visit 方法,ClassVisitor 可以在這些 visit 方法中對(duì)字節(jié)碼進(jìn)行修改,ClassWriter 可以生成最終修改過(guò)的自己字節(jié)碼。
ASM 操作字節(jié)碼案例
接下面我們用幾個(gè)簡(jiǎn)單的例子來(lái)演示 ASM 各個(gè)核心類(lèi)操作字節(jié)碼的案例。
訪問(wèn)類(lèi)的方法和字段
ASM 的 visitor 設(shè)計(jì)模式可以很方便的用來(lái)訪問(wèn)類(lèi)文件中我們感興趣的部分,比如類(lèi)文件的字段和方法列表,有下面的類(lèi):
使用 javac 編譯為 class 文件,可以用下面的 ASM 代碼來(lái)輸出類(lèi)的方法和字段列表:
輸出結(jié)果:
值得注意的是 ClassReader 類(lèi) accept 方法的第二個(gè)參數(shù) flags,這個(gè)參數(shù)是一個(gè)比特掩碼(bit-mask),可以選擇組合的值如下:
SKIP_DEBUG:跳過(guò)類(lèi)文件中的調(diào)試信息,比如行號(hào)信息(LineNumberTable)等
SKIP_CODE:跳過(guò)方法體中的 Code 屬性(方法字節(jié)碼、異常表等)
EXPAND_FRAMES:展開(kāi) StackMapTable 屬性,
SKIP_FRAMES:跳過(guò) StackMapTable 屬性 前面有提到 ClassVisitor 是一個(gè)抽象類(lèi),我們可以選擇關(guān)心的事件進(jìn)行處理,比如例子中的覆寫(xiě)了 visitField 和 visitMethod 方法,僅對(duì)字段和方法進(jìn)行處理,對(duì)于不感興趣的事件可以選擇不覆寫(xiě)或者返回 null 值,這樣 ASM 就知道可以跳過(guò)對(duì)應(yīng)的解析事件了。
使用 Tree Api 的方式也可以實(shí)現(xiàn)同樣的效果
新增一個(gè)字段
在實(shí)際字節(jié)碼轉(zhuǎn)換中,經(jīng)常會(huì)需要給類(lèi)新增一個(gè)字段存儲(chǔ)額外的信息,在 ASM 中給類(lèi)新增一個(gè)字段非常簡(jiǎn)單,以下面的 MyMain 類(lèi)為例,使用 javac 編譯為 class 文件。
那么問(wèn)題來(lái)了,在 ClassVisitor 的哪個(gè)方法里面進(jìn)行添加字段的操作呢?由前面介紹的調(diào)用順序可知,visitField 調(diào)用時(shí)機(jī)只能在 visitInnerClass、visitField、visitMethod、visitEnd 這四種方法中選擇,又因?yàn)?visitInnerClass、visitField 不一定都會(huì)被調(diào)用到,且它們可能被調(diào)用多次,因此放在 visitEnd 方法中進(jìn)行處理比較恰當(dāng)。
使用下面的代碼可以給 MyMain 新增一個(gè) String 類(lèi)型的 xyz 字段。
使用 javap 查看 MyMain2 的字節(jié)碼,可以看到已經(jīng)多了一個(gè)類(lèi)型為String 的 xyz 變量了。
新增方法
在這個(gè)例子中,同樣使用 MyMain 類(lèi)為例,給這個(gè)類(lèi)新增一個(gè) xyz 方法。
新增方法需要調(diào)用 visitMethod 方法,根據(jù)前面的調(diào)用順序來(lái)看,同 visitField 一樣,visitMethod 調(diào)用時(shí)機(jī)只能在 visitInnerClass、visitField、visitMethod、visitEnd 這四種方法中選擇,這里選擇 visitEnd 方法。根據(jù)第一章的內(nèi)容可以知道 xyz 方法的簽名為?(ILjava/lang/String;)V
使用 javap 查看生成的 MyMain2 類(lèi),確認(rèn) xyz 方法已經(jīng)生成:
移除方法和字段
前面介紹了利用 ASM 給 class 文件新增方法和字段,接下來(lái)介紹如何刪掉方法和字段,假設(shè)有 MyMain 類(lèi)代碼如下,下面介紹如何刪掉 abc 字段和 xyz 方法。
如果如果仔細(xì)觀察 ClassVisitor 類(lèi)的 visit 方法,會(huì)發(fā)現(xiàn)visitField、visitMethod 等方法是有返回值的,如果這些方法直接返回 null,效果是這些字段、方法從類(lèi)中被移除。
同樣使用 javap 查看 MyMain2 的字節(jié)碼,可以看到 abc 字段和 xyz 方法已經(jīng)被移除,只剩下 def 字段和 foo 方法了。
小結(jié)
這篇文章我們主要講解了 ASM 字節(jié)碼操作框架,一起來(lái)回顧一下要點(diǎn):
第一,ASM 是一個(gè)久經(jīng)考驗(yàn)的工業(yè)級(jí)字節(jié)碼操作框架。
第二,ASM 的三個(gè)核心類(lèi) ClassReader、ClassVisitor、ClassWriter。ClassReader 對(duì)象創(chuàng)建之后,調(diào)用 ClassReader.accept() 方法,傳入一個(gè) ClassVisitor 對(duì)象。ClassVisitor 在解析字節(jié)碼的過(guò)程中遇到不同的節(jié)點(diǎn)時(shí)會(huì)調(diào)用不同的 visit() 方法。ClassWriter 負(fù)責(zé)把最終修改的字節(jié)碼以 byte 數(shù)組的形式返回。
轉(zhuǎn)載: mp.weixin.qq.com/s/pP6Bt3p82…
本文由mdnice多平臺(tái)發(fā)布