ASM 简介
前言
很早之前就寫過面向切面的編程思想,主要學(xué)習(xí)了AOP的思想(參考:AOP簡(jiǎn)介)以及使用 AspectJ 實(shí)現(xiàn)簡(jiǎn)單的切面編程(參考:AspectJ之切點(diǎn)語法)。
其他常見的AOP編程框架還有 Cglib,Hibernate 和 Spring 等等,而這些目前流行的AOP框架絕大多數(shù)底層實(shí)現(xiàn)都是直接或間接地通過 ASM 來實(shí)現(xiàn)字節(jié)碼操作。
因此,如果你想實(shí)現(xiàn)一些簡(jiǎn)單的切面編程,直接采用上面提及的AOP框架是絕對(duì)可以實(shí)現(xiàn)的,但是這些框架相對(duì)于 ASM 來說重了許多,在你進(jìn)行代碼切入的時(shí)候,可能會(huì)為你引入許多其他包的代碼,導(dǎo)致生成的class文件體積增大不少,因此,對(duì)于一些簡(jiǎn)單的代碼切片,推薦使用 ASM 字節(jié)碼操作庫直接對(duì)class文件動(dòng)態(tài)進(jìn)行代碼切入。
ASM 簡(jiǎn)介
ASM 是一個(gè) Java 字節(jié)碼操控框架。它能被用來動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
簡(jiǎn)單的說,ASM 可以讀取解析class文件內(nèi)容,并提供接口讓你可以對(duì)class文件字節(jié)碼內(nèi)容進(jìn)行CRUD操作······
注: class文件存儲(chǔ)的是java字節(jié)碼,ASM 是對(duì)java字節(jié)碼操作的一層封裝,因此,如果你很了解 class文件格式的話,你甚至可以通過直接使用文本編輯器(eg:Vim)來改寫class文件。
知道了 ASM 的作用后,接下來我們就來看下 ASM 的執(zhí)行模式,了解它的執(zhí)行模式后,我們才能更好地使用。
ASM 框架執(zhí)行流程
ASM 提供了兩組API:Core和Tree:
Core是基于訪問者模式來操作類的
Tree是基于樹節(jié)點(diǎn)來操作類的
本文我們主要討論的是 ASM 的 CoreAPI。
ASM 內(nèi)部采用 訪問者模式 將 .class 類文件的內(nèi)容從頭到尾掃描一遍,每次掃描到類文件相應(yīng)的內(nèi)容時(shí),都會(huì)調(diào)用ClassVisitor內(nèi)部相應(yīng)的方法。
比如:
掃描到類文件時(shí),會(huì)回調(diào)ClassVisitor的visit()方法;
掃描到類注解時(shí),會(huì)回調(diào)ClassVisitor的visitAnnotation()方法;
掃描到類成員時(shí),會(huì)回調(diào)ClassVisitor的visitField()方法;
掃描到類方法時(shí),會(huì)回調(diào)ClassVisitor的visitMethod()方法;
······
掃描到相應(yīng)結(jié)構(gòu)內(nèi)容時(shí),會(huì)回調(diào)相應(yīng)方法,該方法會(huì)返回一個(gè)對(duì)應(yīng)的字節(jié)碼操作對(duì)象(比如,visitMethod()返回MethodVisitor實(shí)例),通過修改這個(gè)對(duì)象,就可以修改class文件相應(yīng)結(jié)構(gòu)部分內(nèi)容,最后將這個(gè)ClassVisitor字節(jié)碼內(nèi)容覆蓋原來.class文件就實(shí)現(xiàn)了類文件的代碼切入。
具體關(guān)系如下:
樹形關(guān)系 使用的接口
Class ClassVisitor
Field FieldVisitor
Method MethodVisitor
Annotation AnnotationVisitor
整個(gè)具體的執(zhí)行時(shí)序如下圖所示:
ASM執(zhí)行流程時(shí)序圖
通過時(shí)序圖可以看出ASM在處理class文件的整個(gè)過程。ASM通過樹這種數(shù)據(jù)結(jié)構(gòu)來表示復(fù)雜的字節(jié)碼結(jié)構(gòu),并利用 Push模型 來對(duì)樹進(jìn)行遍歷。
ASM 中提供一個(gè)ClassReader類,這個(gè)類可以直接由字節(jié)數(shù)組或者class文件間接的獲得字節(jié)碼數(shù)據(jù)。它會(huì)調(diào)用accept()方法,接受一個(gè)實(shí)現(xiàn)了抽象類ClassVisitor的對(duì)象實(shí)例作為參數(shù),然后依次調(diào)用ClassVisitor的各個(gè)方法。字節(jié)碼空間上的偏移被轉(zhuǎn)成各種visitXXX方法。使用者只需要在對(duì)應(yīng)的的方法上進(jìn)行需求操作即可,無需考慮字節(jié)偏移。
這個(gè)過程中ClassReader可以看作是一個(gè)事件生產(chǎn)者,ClassWriter繼承自ClassVisitor抽象類,負(fù)責(zé)將對(duì)象化的class文件內(nèi)容重構(gòu)成一個(gè)二進(jìn)制格式的class字節(jié)碼文件,ClassWriter可以看作是一個(gè)事件的消費(fèi)者。
至此,相信讀者已經(jīng)對(duì) ASM 框架的執(zhí)行過程有一定了解了。接下來我們還剩的一點(diǎn)內(nèi)容就是如何實(shí)現(xiàn)class文件字節(jié)碼的修改。
ASM 字節(jié)碼修改
由于 ASM 是直接對(duì)class文件的字節(jié)碼進(jìn)行操作,因此,要修改class文件內(nèi)容時(shí),也要注入相應(yīng)的java字節(jié)碼。
所以,在注入字節(jié)碼之前,我們還需要了解下class文件的結(jié)構(gòu),JVM指令等知識(shí)。
class文件結(jié)構(gòu)
Java源文件經(jīng)過javac編譯器編譯之后,將會(huì)生成對(duì)應(yīng)的二進(jìn)制.class文件,如下圖所示:
ASM – Javac 流程
Java類文件是 8 位字節(jié)的二進(jìn)制流。數(shù)據(jù)項(xiàng)按順序存儲(chǔ)在class文件中,相鄰的項(xiàng)之間沒有間隔,這使得class文件變得緊湊,減少存儲(chǔ)空間。在Java類文件中包含了許多大小不同的項(xiàng),由于每一項(xiàng)的結(jié)構(gòu)都有嚴(yán)格規(guī)定,這使得 class 文件能夠從頭到尾被順利地解析。
每個(gè)class文件都是有固定的結(jié)構(gòu)信息,而且保留了源碼文件中的符號(hào)。下圖是class文件的格式圖。其中帶 * 號(hào)的表示可重復(fù)的結(jié)構(gòu)。
class文件結(jié)構(gòu)圖
類結(jié)構(gòu)體中所有的修飾符、字符常量和其他常量都被存儲(chǔ)在class文件開始的一個(gè)常量堆棧(Constant Stack)中,其他結(jié)構(gòu)體通過索引引用。
每個(gè)類必須包含headers(包括:class name, super class, interface, etc.)和常量堆棧(Constant Stack)其他元素,例如:字段(fields)、方法(methods)和全部屬性(attributes)可以選擇顯示或者不顯示。
每個(gè)字段塊(Field section)包括名稱、修飾符(public, private, etc.)、描述符號(hào)(descriptor)和字段屬性。
每個(gè)方法區(qū)域(Method section)里面的信息與header部分的信息類似,信息關(guān)于最大堆棧(max stack)和最大本地變量數(shù)量(max local variable numbers)被用于修改字節(jié)碼。對(duì)于非abstract和非native的方法有一個(gè)方法指令表,exceptions表和代碼屬性表。除此之外,還可以有其他方法屬性。
每個(gè)類、字段、方法和方法代碼的屬性有屬于自己的名稱記錄在類文件格式的JVM規(guī)范的部分,這些屬性展示了字節(jié)碼多方面的信息,例如源文件名、內(nèi)部類、簽名、代碼行數(shù)、本地變量表和注釋。JVM規(guī)范允許定義自定義屬性,這些屬性會(huì)被標(biāo)準(zhǔn)的VM(虛擬機(jī))忽略,但是可以包含附件信息。
方法代碼表包含一系列對(duì)java虛擬機(jī)的指令。有些指令在代碼中使用偏移量,當(dāng)指令從方法代碼被插入或者移除時(shí),全部偏移量的值可能需要調(diào)整。
Java類型與class文件內(nèi)部類型對(duì)應(yīng)關(guān)系
Java類型分為基本類型和引用類型,在 JVM 中對(duì)每一種類型都有與之相對(duì)應(yīng)的類型描述,如下表:
Java type JVM 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;
在 ASM 中要獲得一個(gè)類的 JVM 內(nèi)部描述,可以使用org.objectweb.asm.Type類中的getDescriptor(final Class c)方法,如下:
public class TypeDescriptors {
public static void main(String[] args) {
System.out.println(Type.getDescriptor(TypeDescriptors.class));
System.out.println(Type.getDescriptor(String.class));
}
}
運(yùn)行結(jié)果:
Lorg/victorzhzh/core/structure/TypeDescriptors;
Ljava/lang/String;
Java方法聲明與class文件內(nèi)部聲明的對(duì)應(yīng)關(guān)系
在·Java·的二進(jìn)制文件中,方法的方法名和方法的描述都是存儲(chǔ)在Constant pool 中的,且在兩個(gè)不同的單元里。因此,方法描述中不含有方法名,只含有參數(shù)類型和返回類型。
格式:(參數(shù)描述符)返回值描述符
Method declaration in source file Method descriptor
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I]Ljava/lang/Object;
String m() ()Ljava/lang/String;
JVM 指令
假設(shè)現(xiàn)在我們有如下一個(gè)類:
package com.yn.test;
public class Test {
public static void main(String[] agrs){
System.out.println(“Hello World!”);
}
}
我們先用javac com/yn/test/Test.java編譯得到Test.class文件,然后再使用javap -c com/yn/test/Test來查看下這個(gè)Test.class文件的字節(jié)碼,結(jié)果如下圖所示:
Test.class字節(jié)碼
上圖中第3行到第7行,是類Test的默認(rèn)構(gòu)造函數(shù)(由編譯器默認(rèn)生成),Code以下部分是構(gòu)造函數(shù)內(nèi)部代碼,其中:
aload_0: 這個(gè)指令是LOAD系列指令中的一個(gè),它的意思表示裝載當(dāng)前第 0 個(gè)元素到堆棧中。代碼上相當(dāng)于“this”。而這個(gè)數(shù)據(jù)元素的類型是一個(gè)引用類型。這些指令包含了:ALOAD,ILOAD,LLOAD,FLOAD,DLOAD。區(qū)分它們的作用就是針對(duì)不用數(shù)據(jù)類型而準(zhǔn)備的LOAD指令,此外還有專門負(fù)責(zé)處理數(shù)組的指令 SALOAD。
invokespecial: 這個(gè)指令是調(diào)用系列指令中的一個(gè)。其目的是調(diào)用對(duì)象類的方法。后面需要給上父類的方法完整簽名?!?1”的意思是 .class 文件常量表中第1個(gè)元素。值為:“java/lang/Object.""😦)V”。結(jié)合ALOAD_0。這兩個(gè)指令可以翻譯為:“super()”。其含義是調(diào)用自己的父類構(gòu)造方法。
第9到14行是main方法,Code以下是其字節(jié)碼表示:
getstatic: 這個(gè)指令是GET系列指令中的一個(gè)其作用是獲取靜態(tài)字段內(nèi)容到堆棧中。這一系列指令包括了:GETFIELD、GETSTATIC。它們分別用于獲取動(dòng)態(tài)字段和靜態(tài)字段。此處表示的意思獲取靜態(tài)成員System.out到堆棧中。
ldc:這個(gè)指令的功能是從常量表中裝載一個(gè)數(shù)據(jù)到堆棧中。此處表示從常量池中獲取字符串"Hello World!"。
invokevirtual:也是一種調(diào)用指令,這個(gè)指令區(qū)別與 invokespecial 的是它是根據(jù)引用調(diào)用對(duì)象類的方法。此處表示調(diào)用java.io.PrintStream.println(String)方法,結(jié)合前面的操作,這里調(diào)用的就是System.out.println(“Hello World!”)。
return: 這也是一系列指令中的一個(gè),其目的是方法調(diào)用完畢返回:可用的其他指令有:IRETURN,DRETURN,ARETURN等,用于表示不同類型參數(shù)的返回。
更多詳細(xì)內(nèi)容,請(qǐng)參考:JVM字節(jié)碼指令理解,JVM指令,深入字節(jié)碼 – 使用 ASM 實(shí)現(xiàn) AOP
更多字節(jié)碼指令詳情,請(qǐng)參考官網(wǎng):The Java Virtual Machine Instruction Set
接下來,我們就可以根據(jù)上面所講的內(nèi)容,將代碼字節(jié)碼注入到class文件中了。
現(xiàn)在假設(shè)我們想要在類Test的main方法前后動(dòng)態(tài)插入代碼,如下所示:
package com.yn.test;
public class Test {
public static void main(String[] agrs){
System.out.println(“asm insert before”);
System.out.println(“Hello World!”);
System.out.println(“asm insert after”);
}
}
要完成在main方法前后插入輸出代碼,需要以下幾步操作:
讀取Test.class文件,可以通過 ASM 提供的ClassReader類進(jìn)行class文件的讀取與遍歷。
// 使用全限定名,創(chuàng)建一個(gè)ClassReader對(duì)象
ClassReader classReader = new ClassReader(“com.yn.test.Test”);
// 構(gòu)建一個(gè)ClassWriter對(duì)象,并設(shè)置讓系統(tǒng)自動(dòng)計(jì)算棧和本地變量大小
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//創(chuàng)建一個(gè)自定義ClassVisitor,方便后續(xù)ClassReader的遍歷通知
ClassVisitor classVisitor = new TestClassVisitor(classWriter);
//開始掃描class文件
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
構(gòu)造System.out.println(String)的 ASM 代碼。
上面我們從javap反編譯得到的字節(jié)碼可以知道,實(shí)現(xiàn)System.out.println(“Hello World!”);的字節(jié)碼總共需要3步操作:
(1). 獲取System靜態(tài)成員out,其對(duì)應(yīng)的指令為getstatic,對(duì)應(yīng)的 ASM 代碼為:
mv.visitFieldInsn(Opcodes.GETSTATIC,
Type.getInternalName(System.class), //“java/lang/System”
“out”,
Type.getDescriptor(PrintStream.class) //“Ljava/io/PrintStream;”
);
(2). 獲取字符串常量"Hello World!",其對(duì)應(yīng)的指令為ldc,對(duì)應(yīng)的 ASM 代碼為:
mv.visitLdcInsn(“Hello World!”);
(3). 獲取PrintStream.println(String)方法,其對(duì)應(yīng)的指令為invokervirtual,對(duì)應(yīng)的 ASM 代碼為:
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
Type.getInternalName(PrintStream.class), //“java/io/PrintStream”
“println”,
“(Ljava/lang/String;)V”,//方法描述符
false);
在main方法進(jìn)入前,進(jìn)行代碼插入,可以通過MethodVisitor.visitCode()方法。
// 在源方法前去修改方法內(nèi)容,這部分的修改將加載源方法的字節(jié)碼之前
@Override
public void visitCode() {
mv.visitCode();
System.out.println(“method start to insert code”);
sop(“asm insert before”);//this is the insert code
}
在main方法退出前,進(jìn)行代碼插入,可以通關(guān)過MethodVisitor.visitInsn()方法,通過判斷當(dāng)前的指令為return時(shí),表明即將執(zhí)行return語句,此時(shí)插入字節(jié)碼即可。
@Override
public void visitInsn(int opcode) {
//檢測(cè)到return語句
if (opcode == Opcodes.RETURN) {
System.out.println(“method end to insert code”);
sop(“asm insert after”);
}
//執(zhí)行原本語句
mv.visitInsn(opcode);
}
字節(jié)碼插入class文件成功后,導(dǎo)出字節(jié)碼到原文件中。
//獲取改寫后的class二進(jìn)制字節(jié)碼
byte[] classFile = classWriter.toByteArray();
// 將這個(gè)類輸出到原先的類文件目錄下,這是原先的類文件已經(jīng)被修改
File file = new File(“E:/code/Android/Projects/AsmButterknife/sample-java/build/classes/java/main/com/yn/test/Test.class”);
FileOutputStream fos = new FileOutputStream(file);
fos.write(classFile);
fos.close();
至此,我們已經(jīng)完成了對(duì)Test.class的代碼注入。
詳細(xì)代碼請(qǐng)參見:AsmTest
注: asm-commons 包中提供了一個(gè)類AdviceAdapter,使用該類可以更加方便的讓我們?cè)诜椒ㄇ昂笞⑷氪a,因?yàn)槠涮峁┝朔椒╫nMethodEnter()和onMethodExit()。
通過上面介紹的內(nèi)容,我們已經(jīng)成功使用 ASM 動(dòng)態(tài)注入字節(jié)碼到class文件中。但是如果直接采用 ASM 代碼注入字節(jié)碼,還是相對(duì)困難的,幸運(yùn)的是 ASM 給我們提供了 ASMifier 工具,使得我們可以直接通過.class文件反編譯為 ASM 代碼。
因此,當(dāng)我們要使用 ASM 框架往class文件注入字節(jié)碼時(shí),我們通常是將要注入的java源碼先寫出來,然后通過javac編譯出目標(biāo).class文件,然后再通過 ASMifier 工具反編譯該.class文件,得到所需的 ASM 注入代碼。
ASMifier 存在于asm-util.jar中,同時(shí)需要依賴asm.jar,幸運(yùn)的是 ASM 提供了一個(gè)asm-all.jar包,可以方便我們直接運(yùn)行 ASMifier。
asm-all.jar下載地址:asm-all
運(yùn)行命令如下:
java -classpath “asm-all.jar” org.objectweb.asm.util.ASMifier org/domain/package/YourClass.class
如果還嫌上面的操作麻煩,github 上已經(jīng)有人寫了個(gè)前端頁面方便我們將源碼轉(zhuǎn)變?yōu)?ASM 代碼操作:asmifier
參考
AOP 的利器:ASM 3.0 介紹
ASM Bytecode Framework探索與使用
深入字節(jié)碼 – 使用 ASM 實(shí)現(xiàn) AOP
30人點(diǎn)贊
Java
作者:Whyn
鏈接:https://www.jianshu.com/p/a85e8f83fa14
來源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
總結(jié)
- 上一篇: firebird 连接 lazarus
- 下一篇: Linux断点续传