簡介
ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。
Java class 被存儲在嚴格格式定義的 .class 文件里。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。說白了asm是直接通過字節碼來修改class文件。
Spring中的cglib和jdk的動態代理,最底層使用的都是ASM,ASM主要就是使用visitor模式,直接先讀取原來的文件,相當於reader,然后通過adapter,自己定制一套自己的寫法,最后再寫入到一個新的文件中,即相當於改變了原有代碼,增加了切面,這就是動態代理,
這列再簡單提一下訪問者模式:
訪問者模式(VisitorPattern),可以在不修改已有程序結構的前提下,通過添加額外的訪問者來完成對已有代碼功能的提升,它屬於行為模式。其主要目的是將數據結構與數據操作分離
訪問者模式主要由五個角色組成:
抽象訪問者(Visitor)角色:聲明了一個或者多個方法操作,形成所有的具體訪問者角色必須實現的接口。,必須定義 accept(PartA),accept(PartB),accept(PartAll)等所有訪問數據的方法
具體訪問者(ConcreteVisitor)角色:實現抽象訪問者所聲明的接口,也就是抽象訪問者所聲明的各個訪問操作。比如 VisitorA,VisitorB,只會實現訪問對應的數據
抽象節點(Node)角色:聲明一個接受操作,接受一個訪問者對象作為一個參數。
具體節點(ConcreteNode)角色:實現了抽象節點所規定的接受操作。,比如 PartA,PartB
結構對象(ObjectStructure)角色:有如下的責任,可以遍歷結構中的所有元素。PartAll
注意:訪問者對象,適合part穩定不變的情況下,只需要新增Visitor即可
基本使用
1. 讀取 ClassReader
- 構造器
ClassReader有四種構造器
比如:
// 根據類的全限定名獲取
final ClassReader classReader = new ClassReader("com/hou/api/controller/TcmUserController");
- accept方法
accept是主要使用的方法,主要有兩個:
第一個參數是傳入的訪問者對象,第二個int是讀取模式,有四個值:
2. 訪問-ClassVisitor
ClassVisitor是訪問者模式的抽象接口,接口中定義了訪問class各個部分的方法,比如訪問方法,訪問注解等,方法如下:
構造器只需要ASM API版本(在Opcodes中可以找到,1-9),或者再加上另一個ClassVisitor用於一起解析
當Visitor被傳入accept之后,ClassReader會按順序調用, 即調用所有的訪問方法,同時會根據構造器傳入的讀取模式判斷是否執行,如源代碼:
當所有信息都訪問結束,調用visitEnd,,其中防范字段或者注解等,會使用其他訪問者抽象類的實現,比如AnnotationVisitor等,這里的具體調用,后面再分析
3. 訪問注解-AnnotationVisitor
AnnotationVisitor是用於解析注釋信息的抽象類,主要定義了四個訪問方法:
- visit:傳入注釋方法名稱和值,值必須是基本類型(基本數字、char及其數組,String和類)
- visitArray:傳入注釋方法名稱,返回另一個AnnotationVisitor。這個新的Visitor會被傳入數組內的值,所有的name傳入都為null。
- visitAnnotation:傳入注釋方法名稱和值的描述符,返回的是值的AnnotationVisitor。
- visitEnum:傳入注釋方法名、值的描述符和枚舉名稱。
4. 訪問變量-FieldVisitor
FieldVisitor是用來訪問字段的抽象類,定義的方法比較簡單(他就類似只能訪問某個部分的訪問者,classvisitor可以訪問所有部分),除了visitEnd在最后調用外,比較常用的就是visitAnnotation和visitTypeAnnotation。這些方法的使用都和ClassVisitor的使用差不多,
5. 訪問方法-MethodVisitor
這個是非常重要的一個訪問者,放到本文后面說
使用案例
上面看了那么多訪問者模式定義的頂層接口和抽象類,那么這玩意到底有啥用,或者具體是如何使用的,下面就來寫幾個例子:
1. 解析一個類
原始類:
@Component("test")
public class MyLift {
private String name;
private static Integer age=27;
}
- 首先需要實現訪問者接口,ClassVisitor,同時選擇自己需要的解析信息,去覆寫對應的方法,同時,在有返回值的方法,需要返回一個具體訪問者的實例,比如AnnotationVisitor,所以還需要去繼承此類並實現
public class MyClassVisitor extends ClassVisitor {
//構造器必須要傳入 ASM版本
public MyClassVisitor(int api) {
super(api);
}
/**
* 因為我們需要訪問類的名稱,字段和注解,所以需要覆寫以下三個方法
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println("類名:"+name);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println("注解:"+descriptor);
// 這里需要返回我們自定義的注解訪問者,由它去解析注解
return new MyAnnotationVisitor(Opcodes.ASM9);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("字段:"+name+", 值"+value);
// 這里需要返回我們的字段訪問者,由它去解析字段
return new MyFieldVisitor(Opcodes.ASM9);
}
}
- 自定義具體的字段和注解訪問者實例
/**
* 如果沒有額外操作,可以暫時不覆寫方法
*/
public class MyAnnotationVisitor extends AnnotationVisitor {
public MyAnnotationVisitor(int api) {
super(api);
}
// 覆寫注解訪問的對應方法
@Override
public void visit(String name, Object value) {
System.out.println("注解name:"+name);
System.out.println("注解value:"+value);
super.visit(name, value);
}
}
public class MyFieldVisitor extends FieldVisitor {
public MyFieldVisitor(int api) {
super(api);
}
}
- 執行解析
2. 生成一個類
-
在生成類之前,需要了解到訪問標志:
訪問標志是用於JVM訪問類、字段、方法檢查和調用的一個int。這些標志既包含了我們常見的public這種訪問限定符,還包含了static、final這種修飾符,除此之外還有聲明類為接口的interface,為枚舉的enum
所有標志都在Opcodes類中定義為常量(值都是用16進制表示), 常用的訪問標志如:- 訪問限定(選擇其中一個或無):ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED
- 類聲明(選擇一個):ACC_INTERFACE,int ACC_ENUM
- 類修飾(選擇一個或沒有):ACC_FINAL(類為enum必選),ACC_ABSTRACT(類為interface必選)
- 方法修飾(除了沖突外可以任選):static,final,abstract(在接口或抽象類里面使用,與static,final,native等沖突),synchronized,strict(關鍵詞是strictfp,精度保留,只是口頭保證罷了),native(本地方法,JNI調用)
注意:這些常量可以用or疊加修飾,如果訪問標志不合法(比如吧ACC_PUBLIC和ACC_PRIVATE用or聯系起來當了訪問標志),在ASM寫入時是不會報錯的,但是在JVM試圖加載這個類的時候可能會拋出ClassFormatError
-
生成類我們用到的是ClassWriter,它本質上就是ClassVisitor,我們只要用可以構建類的數據按照剛才的格式傳給它就能生成對應的類
public class AsmTest {
public static void main(String[] args) throws IOException {
// 生成類
final ClassWriter classWriter = new ClassWriter(0);
// 生成類,使用visit方法,參數對應: jdk版本,訪問標志,類的全限定名, 泛型,父類全限定名,接口
classWriter.visit(V1_8, ACC_PROTECTED + ACC_FINAL,"com/hou/api/controller/MyLift1",null,"java/lang/Object",null);
//javac編譯時會把沒有定義構造函數的普通類加入默認的構造函數,這里先不生成方法和構造器
// 生成類的注解
classWriter.visitAnnotation("org/springframework/stereotype/Component",true);
// 寫入字段, 生成一個 public final static
FieldVisitor fv = classWriter.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "name", "Ljava/lang/String;",null, "houzheng");
fv.visitEnd();
classWriter.visitEnd(); // 生成需要調用結束方法
final byte[] bytes = classWriter.toByteArray();
// 寫入文件后反編譯查看,注意:生成的是class文件
fileToBytes(bytes,"D:/","MyLife1.class");
}
反編譯查看:
3. 修改類
修改類需要ClassReader和ClassWriter互相配合。利用ClassVisitor等進行數據的轉移和修改
public class AsmTest {
public static void main(String[] args) throws IOException {
ClassWriter classWriter = new ClassWriter(0);
ClassReader classReader = new ClassReader("com/hou/api/controller/MyLift");
//MyClassVisitor 構造器傳入一個ClassWriter,這樣,ClassReader傳入的信息可以直接寫到ClassWriter里面,我們只需要修改我們所需要的方法就可以達到修改的效果,而不用將所有ClassVisitor的方法實現
classReader.accept(new MyClassVisitor(Opcodes.ASM9,classWriter),ClassReader.EXPAND_FRAMES);
// 將讀取到並修改后的文件輸出寫入文件
final byte[] bytes = classWriter.toByteArray();
// 寫入文件后反編譯查看,注意:生成的是class文件
fileToBytes(bytes,"D:/","MyLife2.class");
}
反編譯查看: