ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者加強既有類的功能。ASM 能夠直接產生二進制 class 文件,也能夠在類被加載入 Java 虛擬機以前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的全部元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,可以改變類行爲,分析類信息,甚至可以根據用戶要求生成新類。html
想象一下,若是開源框架要求你添加各類Java類來實現諸如log、cache、transaction等功能,我想這個開源框架你確定不會用吧。動態生成類能夠減小對你代碼的侵入,提升使用者的效率。java
最直接的改造 Java 類的方法莫過於直接改寫 class 文件。Java 規範詳細說明了 class 文件的格式,直接編輯字節碼確實能夠改變 Java 類的行爲。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 文件動手術。是的,這是最直接的方法,可是要求使用者對 Java class 文件的格式了熟於心:當心地推算出想改造的函數相對文件首部的偏移量,同時從新計算 class 文件的校驗碼以經過 Java 虛擬機的安全機制。git
能夠發現,直接操做class文件是比較麻煩的,就跟爲何咱們都選擇使用框架同樣,框架屏蔽了底層的複雜性。ASM就是操做class的一把利器。github
ASM提供了兩種API:編程
區別是CoreAPI基於事件模型,定義了Class中各個元素的Visitor,不須要加載整個Class到內存中。而TreeAPI以Tree結構將Class整個結構讀取到內存中。從使用角度來講TreeAPI更爲簡單。數組
如下示例採用的是CoreAPI方式。安全
添加Maven:bash
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.0.4</version>
</dependency>
複製代碼
使用的相對比較穩定,使用比較多的版本5.0.4。app
首先說明一下,修改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 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);
MethodVisitor mv = super.visitMethod(access, name, desc, signature, 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, String name, String desc, boolean b) {
//當開始初始化構造函數時,先訪問父類構造函數,相似源碼中的super()
if (opcode==Opcodes.INVOKESPECIAL && name.equals("<init>")){
owner = superClassName;
}
super.visitMethodInsn(opcode, owner, name, desc, 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, 0, 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。
可是那麼多指令、OpCode、符號怎麼記得住呢?好比上面代碼中的:
visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
visitVarInsn(Opcodes.LSTORE, 1);
複製代碼
Opcodes.INVOKESTATIC 、Opcodes.LSTORE、()J,是否是看着就暈?其實除了熟能生巧外,還可使用工具。
若是你使用的是IDEA,那麼能夠安裝上ASM Bytecode Outline 2017插件。而後在源文件上右鍵選擇Show Bytecode Outline,你將會看到以下視圖:
切換的ASMified視圖,你會看到跟咱們上面寫的同樣的代碼,直接Copy過來使用便可。
查看示例完整源代碼:asm_demo
參考資料: