Java字節(jié)碼操縱框架ASM圖文實(shí)例詳解
引言
今天我們將介紹字節(jié)碼相關(guān)的應(yīng)用場(chǎng)景,首先要介紹的是如何對(duì)字節(jié)碼做解析和修改,本文將會(huì)詳細(xì)給大家介紹一個(gè)工業(yè)級(jí)字節(jié)碼操作框架 ASM。
當(dāng)我們需要對(duì)一個(gè) class 文件做修改時(shí),我們可以選擇自己解析這個(gè)class 文件,在符合 Java 字節(jié)碼規(guī)范的前提下進(jìn)行字節(jié)碼改造。如果你寫過(guò) class 文件的解析程序,會(huì)發(fā)現(xiàn)這個(gè)過(guò)程極其繁瑣,更別說(shuō)進(jìn)行增加方法等操作了。
ASM 最開始是 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)代理的代理類。經(jīng)過(guò)多年的發(fā)展,ASM 在諸多框架中已經(jīng)遍地開花,成為字節(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)有很多著名的開源框架都在使用,例如 cglib,、mybatis、fastjson
其它字節(jié)碼操作框架在操作字節(jié)碼的過(guò)程中生成大量的中間類和對(duì)象,耗費(fèi)大量的內(nèi)存且運(yùn)行緩慢,ASM 使用了訪問(wèn)者(Visitor)設(shè)計(jì)模式,避免了創(chuàng)建和消耗大量的中間變量。
ASM 提供了兩種生成和轉(zhuǎn)換類的方法: 基于事件觸發(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ú)法在不重新開始的情況下再次讀取,ASM 的 Core API 類似于這種方式。
DOM 解析方式則會(huì)將整個(gè) XML 作為類似樹結(jié)構(gòu)的方式讀入內(nèi)存中以便操作及解析,ASM 的 Tree API 類似于這種方式。
以下面的 XML 文件為例:
<Order> <Customer>Arthur</Customer> <Product> <Name>Birdsong Clock</Name> <Quantity>12</Quantity> <Price currency="USD">21.95</Price > </Product> </Order>
對(duì)應(yīng)的 SAX 和 DOM 解析方式的如下圖所示:
ASM 核心類介紹
ClassReader
它是字節(jié)碼讀取和分析引擎,幫我們做了最苦最累的解析二進(jìn)制的 class 文件字節(jié)碼的活。采用類似于 SAX 的事件讀取機(jī)制,每當(dāng)有事件發(fā)生時(shí),觸發(fā)相應(yīng)的 ClassVisitor、MethodVisitor 等做相應(yīng)的處理。
ClassVisitor
它是一個(gè)抽象類,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 類中的 visit 方法必須按照以下的順序被調(diào)用執(zhí)行:
visit [visitSource] [visitOuterClass] (visitAnnotation | visitAttribute)* (visitInnerClass | visitField | visitMethod)* visitEnd
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è)類是 ClassVisitor 抽象類的一個(gè)實(shí)現(xiàn)類,其之前的每個(gè) ClassVisitor 都可能對(duì)原始的字節(jié)碼做修改,ClassWriter 的 toByteArray 方法則把最終修改的字節(jié)碼以 byte 數(shù)組的形式返回
這三個(gè)核心類的關(guān)系如下圖
一個(gè)最簡(jiǎn)單的用法如下面的代碼所示:
public class FooClassVisitor extends ClassVisitor { ... // visitXXX() 函數(shù) ... } ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new FooClassVisitor(cw); cr.accept(cv, 0);
上面的代碼中,ClassReader 負(fù)責(zé)讀取類文件字節(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è)核心類操作字節(jié)碼的案例。
訪問(wèn)類的方法和字段
ASM 的 visitor 設(shè)計(jì)模式可以很方便的用來(lái)訪問(wèn)類文件中我們感興趣的部分,比如類文件的字段和方法列表,有下面的類:
public class MyMain { public int a = 0; public int b = 1; public void test01() { } public void test02() { } }
使用 javac 編譯為 class 文件,可以用下面的 ASM 代碼來(lái)輸出類的方法和字段列表:
byte[] bytes = getBytes(); // MyMain.class 文件的字節(jié)數(shù)組 ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(0); ClassVisitor cv = new ClassVisitor(ASM5, cw) { @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { System.out.println("field: " + name); return super.visitField(access, name, desc, signature, value); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { System.out.println("method: " + name); return super.visitMethod(access, name, desc, signature, exceptions); } }; cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
輸出結(jié)果:
field: a
field: b
method: <init>
method: test01
method: test02
值得注意的是 ClassReader 類 accept 方法的第二個(gè)參數(shù) flags,這個(gè)參數(shù)是一個(gè)比特掩碼(bit-mask),可以選擇組合的值如下:
- SKIP_DEBUG:跳過(guò)類文件中的調(diào)試信息,比如行號(hào)信息(LineNumberTable)等
- SKIP_CODE:跳過(guò)方法體中的 Code 屬性(方法字節(jié)碼、異常表等)
- EXPAND_FRAMES:展開 StackMapTable 屬性,
- SKIP_FRAMES:跳過(guò) StackMapTable 屬性
前面有提到 ClassVisitor 是一個(gè)抽象類,我們可以選擇關(guān)心的事件進(jìn)行處理,比如例子中的覆寫了 visitField 和 visitMethod 方法,僅對(duì)字段和方法進(jìn)行處理,對(duì)于不感興趣的事件可以選擇不覆寫或者返回 null 值,這樣 ASM 就知道可以跳過(guò)對(duì)應(yīng)的解析事件了。
使用 Tree Api 的方式也可以實(shí)現(xiàn)同樣的效果
byte[] bytes = getBytes(); ClassReader cr = new ClassReader(bytes); ClassNode cn = new ClassNode(); cr.accept(cn, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE); List<FieldNode> fields = cn.fields; for (int i = 0; i < fields.size(); i++) { FieldNode fieldNode = fields.get(i); System.out.println("field: " + fieldNode.name); } List<MethodNode> methods = cn.methods; for (int i = 0; i < methods.size(); ++i) { MethodNode method = methods.get(i); System.out.println("method: " + method.name); } ClassWriter cw = new ClassWriter(0); cr.accept(cn, 0); byte[] bytesModified = cw.toByteArray();
新增一個(gè)字段
在實(shí)際字節(jié)碼轉(zhuǎn)換中,經(jīng)常會(huì)需要給類新增一個(gè)字段存儲(chǔ)額外的信息,在 ASM 中給類新增一個(gè)字段非常簡(jiǎn)單,以下面的 MyMain 類為例,使用 javac 編譯為 class 文件。
public class MyMain { }
那么問(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 類型的 xyz 字段。
byte[] bytes = FileUtils.readFileToByteArray(new File("./MyMain.class")); ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(0); ClassVisitor cv = new ClassVisitor(ASM5, cw) { @Override public void visitEnd() { super.visitEnd(); FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC, "xyz", "Ljava/lang/String;", null, null); if (fv != null) fv.visitEnd(); } }; cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); byte[] bytesModified = cw.toByteArray(); FileUtils.writeByteArrayToFile(new File("./MyMain2.class"), bytesModified);
使用 javap 查看 MyMain2 的字節(jié)碼,可以看到已經(jīng)多了一個(gè)類型為String 的 xyz 變量了。
... public java.lang.String xyz; descriptor: Ljava/lang/String; flags: ACC_PUBLIC ...
新增方法
在這個(gè)例子中,同樣使用 MyMain 類為例,給這個(gè)類新增一個(gè) xyz 方法。
public void xyz(int a, String b) { }
新增方法需要調(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
byte[] bytes = FileUtils.readFileToByteArray(new File("./MyMain.class")); ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(0); ClassVisitor cv = new ClassVisitor(ASM5, cw) { @Override public void visitEnd() { super.visitEnd(); MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "xyz", "(ILjava/lang/String;)V", null, null); if (mv != null) mv.visitEnd(); } }; cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); byte[] bytesModified = cw.toByteArray(); FileUtils.writeByteArrayToFile(new File("./MyMain2.class"), bytesModified);
使用 javap 查看生成的 MyMain2 類,確認(rèn) xyz 方法已經(jīng)生成:
... public void xyz(int, java.lang.String); descriptor: (ILjava/lang/String;)V flags: ACC_PUBLIC ...
移除方法和字段
前面介紹了利用 ASM 給 class 文件新增方法和字段,接下來(lái)介紹如何刪掉方法和字段,假設(shè)有 MyMain 類代碼如下,下面介紹如何刪掉 abc 字段和 xyz 方法。
public class MyMain { private int abc = 0; private int def = 0; public void foo() { } public int xyz(int a, String b) { return 0; } }
如果如果仔細(xì)觀察 ClassVisitor 類的 visit 方法,會(huì)發(fā)現(xiàn)visitField、visitMethod 等方法是有返回值的,如果這些方法直接返回 null,效果是這些字段、方法從類中被移除。
byte[] bytes = FileUtils.readFileToByteArray(new File("./MyMain.class")); ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(0); ClassVisitor cv = new ClassVisitor(ASM5, cw) { @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if ("abc".equals(name)) { return null; } return super.visitField(access, name, desc, signature, value); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if ("xyz".equals(name)) { return null; } return super.visitMethod(access, name, desc, signature, exceptions); } }; cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); byte[] bytesModified = cw.toByteArray(); FileUtils.writeByteArrayToFile(new File("./MyMain2.class"), bytesModified);
同樣使用 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è)核心類 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ù)組的形式返回。
以上就是Java字節(jié)碼操縱框架ASM圖文實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于Java 字節(jié)碼操縱框架ASM的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MyBatis查詢數(shù)據(jù),賦值給List集合時(shí),數(shù)據(jù)缺少的問(wèn)題及解決
這篇文章主要介紹了MyBatis查詢數(shù)據(jù),賦值給List集合時(shí),數(shù)據(jù)缺少的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01MybatisPlus查詢數(shù)據(jù)日期格式化問(wèn)題解決方法
MyBatisPlus是MyBatis的增強(qiáng)工具,支持常規(guī)的CRUD操作以及復(fù)雜的聯(lián)表查詢等功能,這篇文章主要給大家介紹了關(guān)于MybatisPlus查詢數(shù)據(jù)日期格式化問(wèn)題的解決方法,需要的朋友可以參考下2023-10-10Spring Boot項(xiàng)目集成UidGenerato的方法步驟
這篇文章主要介紹了Spring Boot項(xiàng)目集成UidGenerato的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12SpringBoot利用filter實(shí)現(xiàn)xss防御功能
Cross-Site?Scripting(跨站腳本攻擊)簡(jiǎn)稱?XSS,是一種代碼注入攻擊,攻擊者通過(guò)在目標(biāo)網(wǎng)站上注入惡意腳本,使之在用戶的瀏覽器上運(yùn)行,利用這些惡意腳本,攻擊者可獲取用戶的敏感信息,本文給大家介紹了SpringBoot利用filter實(shí)現(xiàn)xss防御功能,需要的朋友可以參考下2024-09-09基于JWT實(shí)現(xiàn)SSO單點(diǎn)登錄流程圖解
這篇文章主要介紹了基于JWT實(shí)現(xiàn)SSO單點(diǎn)登錄流程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07