深入開源框架底層之ASM
什麼是 ASM ?
ASM 是一個 Java 位元組碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class 檔案裡,這些類檔案擁有足夠的元資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM 從類檔案中讀入資訊後,能夠改變類行為,分析類資訊,甚至能夠根據使用者要求生成新類。
為什麼要動態生成 Java 類?
想象一下,如果開源框架要求你新增各種Java類來實現諸如log、cache、transaction等功能,我想這個開源框架你肯定不會用吧。動態生成類可以減少對你程式碼的侵入,提高使用者的效率。
為什麼選擇ASM?
最直接的改造 Java 類的方法莫過於直接改寫 class 檔案。Java 規範詳細說明瞭 class 檔案的格式,直接編輯位元組碼確實可以改變 Java 類的行為。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 檔案動手術。是的,這是最直接的方法,但是要求使用者對 Java class 檔案的格式了熟於心:小心地推算出想改造的函式相對檔案首部的偏移量,同時重新計算 class 檔案的校驗碼以通過 Java 虛擬機器器的安全機制。
可以發現,直接操作class檔案是比較麻煩的,就跟為什麼我們都選擇使用框架一樣,框架遮蔽了底層的複雜性。ASM就是操作class的一把利器。
使用 ASM 程式設計
ASM提供了兩種API:
- CoreAPI(ClassVisitor 、MethodVisitor等)
- TreeAPI(ClassNode,MethodNode等)
區別是CoreAPI基於事件模型,定義了Class中各個元素的Visitor,不需要載入整個Class到記憶體中。而TreeAPI以Tree結構將Class整個結構讀取到記憶體中。從使用角度來說TreeAPI更為簡單。
以下示例採用的是CoreAPI方式。
新增Maven:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.0.4</version>
</dependency>
複製程式碼
使用的相對比較穩定,使用比較多的版本5.0.4。
首先說明一下,修改Class有多種方式,例如直接修改當前Class,或者生成Class的子類,從而達到增強的效果。
下面的示例就是通過生成指定Class的子類,從而達到增強的效果,好處是對原有Class無侵入,並且可以實現多型的效果。
首先定義一個我們要增強的類:
package com.zjz;
import java.util.Random;
/**
* @author zhaojz created at 2019-08-22 10:49
*/
public class Student {
public String name;
public void studying() throws InterruptedException {
System.out.println(this.name+"正在學習...");
Thread.sleep(new Random().nextInt(5000));
}
}
複製程式碼
接下來首先定義一個ClassReader:
ClassReader classReader = new ClassReader("com.zjz.Student");
複製程式碼
然後再定義一個ClassWriter:
ClassWriter classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS);
複製程式碼
ClassWriter.COMPUTE_MAXS 表示自動計算區域性變數和運算元棧大小。更多其它選項可參考:asm.ow2.io
接下來開始正式訪問Class:
//通過ClassVisitor訪問Class(匿名類的方式,可以自行定義為一個獨立的類)
//ASM5為JVM位元組碼指令操作碼
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5,classWriter) {
//宣告一個全域性變數,表示增強後生成的子類的父類
String enhancedSuperName;
@Override
public void visit(int version,int access,String name,String signature,String superName,String[] interfaces) {
//拼接需要生成的子類的類名:Student$EnhancedByASM
String enhancedName = name+"$EnhancedByASM";
//將Student設定為父類
enhancedSuperName = name;
super.visit(version,access,enhancedName,signature,enhancedSuperName,interfaces);
}
@Override
public FieldVisitor visitField(int access,String desc,Object value) {
//這裡是演示欄位訪問
System.out.println("Field:" + name);
return super.visitField(access,name,desc,value);
}
@Override
public MethodVisitor visitMethod(int access,String[] exceptions) {
System.out.println("Method:" + name);
MethodVisitor mv = super.visitMethod(access,exceptions);
MethodVisitor wrappedMv = mv;
//判斷當前讀取的方法
if (name.equals("studying")) {
//如果是studying方法,則包裝一個方法的Visitor
wrappedMv = new StudentStudyingMethodVisitor(Opcodes.ASM5,mv);
}else if(name.equals("<init>")){
//如果是構造方法,處理子類中父類的建構函式呼叫
wrappedMv = new StudentEnhancedConstructorMethodVisitor(Opcodes.ASM5,mv,enhancedSuperName);
}
return wrappedMv;
}
};
複製程式碼
接下來重點看看MethodVisitor:
//Studying方法的Visitor
static class StudentStudyingMethodVisitor extends MethodVisitor{
public StudentStudyingMethodVisitor(int i,MethodVisitor methodVisitor) {
super(i,methodVisitor);
}
//MethodVisitor 中定義了不同的visitXXX()方法,代表的不同的訪問階段。
//visitCode表示剛剛進入方法。
@Override
public void visitCode() {
//新增一行System.currentTimeMillis()呼叫
visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false);
//並且將其儲存在區域性變量表內位置為1的地方
visitVarInsn(Opcodes.LSTORE,1);
//上面兩個的作用就是在Studying方法的第一行新增 long start = System.currentTimeMillis()
}
//visitInsn 表示訪問進入了方法內部
@Override
public void visitInsn(int opcode) {
//通過opcode可以得知當前訪問到了哪一步,如果是>=Opcodes.IRETURN && opcode <= Opcodes.RETURN 表明方法即將退出
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)){
//載入區域性變量表中位置為1的資料,也就是start的資料,並傳入給下面的方法
visitVarInsn(Opcodes.LLOAD,1);
//然後呼叫自定義的一個工具方法,用來輸出耗時
visitMethodInsn(Opcodes.INVOKESTATIC,"com/zjz/Before","end","(J)V",false);
}
super.visitInsn(opcode);
}
}
static class StudentEnhancedConstructorMethodVisitor extends MethodVisitor{
//定義一個全域性變數記錄父類名稱
private String superClassName;
public StudentEnhancedConstructorMethodVisitor(int i,MethodVisitor methodVisitor,String superClassName) {
super(i,methodVisitor);
this.superClassName = superClassName;
}
@Override
public void visitMethodInsn(int opcode,String owner,boolean b) {
//當開始初始化建構函式時,先訪問父類建構函式,類似原始碼中的super()
if (opcode==Opcodes.INVOKESPECIAL && name.equals("<init>")){
owner = superClassName;
}
super.visitMethodInsn(opcode,owner,b);
}
}
複製程式碼
此時ClassVisitor還沒有資料的輸入,只定義了資料的輸出 new ClassVisitor(Opcodes.ASM5,classWriter),所以還需要:
classReader.accept(classVisitor,ClassReader.SKIP_DEBUG);
複製程式碼
到此就完成了Class的讀取,訪問修改,輸出的過程。
細心的觀眾就會發現了,輸出到哪裡了?怎麼樣訪問新生成的類呢?所以我們需要定義一個ClassLoader來載入我們生成的Class:
static class StudentClassLoader extends ClassLoader{
public Class defineClassFromClassFile(String className,byte[] classFile) throws ClassFormatError{
return defineClass(className,classFile,classFile.length);
}
}
複製程式碼
然後通過ClassWriter獲取新生成的類的位元組陣列,並載入到JVM中:
byte[] data = classWriter.toByteArray();
Class subStudent = classLoader.defineClassFromClassFile("com.zjz.Student$EnhancedByASM",data);
複製程式碼
到此就完成了一個class的生成,上面的程式碼完成的是一個很簡單的事情:記錄學習時間。
總結一下:
ASM CoreAPI 核心的三個東西就是ClassReader、Visitor、ClassWriter,通過責任鏈模式將其連結起來。
Visitor通過訪問者模式進行方法、欄位等等屬性的訪問,如果需要修改一個方法和欄位,只需要將其原本的Visitor給Wrap一下即可。
關於如何進行程式碼的hook需要理解JVM相關位元組碼指令,以及ASM的相關OpCode。
ASM Bytecode Outline 2017
但是那麼多指令、OpCode、符號怎麼記得住呢?比如上面程式碼中的:
visitMethodInsn(Opcodes.INVOKESTATIC,false);
visitVarInsn(Opcodes.LSTORE,1);
複製程式碼
Opcodes.INVOKESTATIC 、Opcodes.LSTORE、()J,是不是看著就暈?其實除了熟能生巧外,還可以使用工具。
如果你使用的是IDEA,那麼可以安裝上ASM Bytecode Outline 2017外掛。然後在原始檔上右鍵選擇Show Bytecode Outline,你將會看到如下檢視:
切換的ASMified檢視,你會看到跟我們上面寫的一樣的程式碼,直接Copy過來使用即可。
檢視示例完整原始碼:asm_demo
參考資料: