1、ASM概述
- ASM是一個功能比較齊全的java字節(jié)碼操作與分析框架掉弛,通過ASM框架症见,我們可以動態(tài)的生成類或者增強已有類的功能。
- ASM可以直接生成二進制.class文件殃饿,也可以在類被加載入java虛擬機之前動態(tài)改變現(xiàn)有類的行為谋作。
- java的二進制文件被存儲在嚴格格式定義的.class文件里,這些字節(jié)碼文件擁有足夠的元數(shù)據(jù)信息用來表示類中的所有元素乎芳,包括類名稱遵蚜、方法、屬性以及java字節(jié)碼指令奈惑。ASM從字節(jié)碼文件讀入這些信息后吭净,能夠改變類行為、分析類的信息肴甸,甚至還可以根據(jù)具體的要求生成新的類寂殉。
- ASM 通過樹這種數(shù)據(jù)結(jié)構(gòu)來表示復雜的字節(jié)碼結(jié)構(gòu),因為需要處理字節(jié)碼結(jié)構(gòu)是固定的原在,所以可以利用Visitor(訪問者) 設(shè)計模式來對樹進行遍歷友扰,在遍歷過程中對字節(jié)碼進行修改。
2庶柿、Java 類文件概述
所謂 Java 類文件村怪,就是通常用 javac 編譯器產(chǎn)生的 .class 文件。這些文件具有嚴格定義的格式浮庐。Java 源文件經(jīng)過 javac 編譯器編譯之后甚负,將會生成對應(yīng)的二進制文件。
Java 類文件是 8 位字節(jié)的二進制流审残。數(shù)據(jù)項按順序存儲在 class 文件中梭域,相鄰的項之間沒有間隔,這使得 class 文件變得緊湊维苔,減少存儲空間碰辅。一個簡單的Hello World程序
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
經(jīng)過 javac 編譯后,得到的類文件HelloWorld.class介时,該文件中是由十六進制符號組成的没宾,這一段十六進制符號組成的長串是嚴格遵守 Java 虛擬機規(guī)范。用vim查看HelloWorld.class
vim HelloWorld.class
打開文件后輸入
:%!xxd
按回車即可看到如下一串串十六進制符號
HelloWorld.class文件構(gòu)成如下:
從上圖中可以看到沸柔,一個 Java 類文件大致可以歸為 10 個項:
Magic:該項存放了一個 Java 類文件的魔數(shù)(magic number)循衰,一個 Java 類文件的前 4 個字節(jié)被稱為它的魔數(shù)。每個正確的 Java 類文件都是以 0xCAFEBABE 開頭的褐澎,這樣保證了 Java 虛擬機能很輕松的分辨出 Java 文件和非 Java 文件会钝。
有趣的是,魔數(shù)的固定值是Java之父James Gosling制定的,為CafeBabe(咖啡寶貝)迁酸,而Java的圖標為一杯咖啡先鱼。
Version:該項存放了 Java 類文件的版本信息
Constant Pool:常量池中存儲兩類常量:字面量與符號引用。字面量為文本字符串和代碼中聲明為Final的常量值奸鬓,符號引用如類和接口的全局限定名焙畔、字段的名稱和描述符、方法的名稱和描述符串远。
Access_flag:該項指明了該文件中定義的是類還是接口(一個 class 文件中只能有一個類或接口)宏多,同時還指明了類或接口的訪問標志,如 public澡罚,private, abstract 等信息伸但。
This Class:指向表示該類全限定名稱的字符串常量的指針。
Super Class:指向表示父類全限定名稱的字符串常量的指針留搔。
Interfaces:一個指針數(shù)組更胖,存放了該類或父類實現(xiàn)的所有接口名稱的字符串常量的指針。
Fields:該項對類或接口中聲明的字段進行了細致的描述催式。需要注意的是函喉,fields 列表中僅列出了本類或接口中的字段,并不包括從超類和父接口繼承而來的字段荣月。
Methods:該項對類或接口中聲明的方法進行了細致的描述。例如方法的名稱梳毙、參數(shù)和返回值類型等哺窄。需要注意的是,methods 列表里僅存放了本類或本接口中的方法账锹,并不包括從超類和父接口繼承而來的方法萌业。
Class attributes:該項存放了在該文件中類或接口所定義的屬性的基本信息。
3奸柬、ASM庫的結(jié)構(gòu)
Core:為其他包提供基礎(chǔ)的讀生年、寫、轉(zhuǎn)化Java字節(jié)碼和定義的API廓奕,并且可以生成Java字節(jié)碼和實現(xiàn)大部分字節(jié)碼的轉(zhuǎn)換抱婉。
Tree:提供了 Java 字節(jié)碼在內(nèi)存中的表現(xiàn)
Commons:提供了一些常用的簡化字節(jié)碼生成、轉(zhuǎn)換的類和適配器
Util:包含一些幫助類和簡單的字節(jié)碼修改類桌粉,有利于在開發(fā)或者測試中使用
XML:提供一個適配器將XML和SAX-comliant轉(zhuǎn)化成字節(jié)碼結(jié)構(gòu)蒸绩,可以允許使用XSLT去定義字節(jié)碼轉(zhuǎn)化
4、ASM Core API
ClassReader:這個類會將 .class 文件讀入到 ClassReader 中的字節(jié)數(shù)組中铃肯,它的 accept 方法接受一個 ClassVisitor 實現(xiàn)類患亿,并按照順序調(diào)用 ClassVisitor 中的方法
ClassVisitor:主要負責訪問類的成員信息。包括標記在類上的注解押逼、類的構(gòu)造方法步藕、類的字段惦界、類的方法、靜態(tài)代碼塊等
ClassWriter:ClassWriter 是一個 ClassVisitor 的子類咙冗,是和 ClassReader 對應(yīng)的類表锻,ClassReader 是將 .class 文件讀入到一個字節(jié)數(shù)組中,ClassWriter 是將修改后的類的字節(jié)碼內(nèi)容以字節(jié)數(shù)組的形式輸出乞娄。
AdviceAdapter:MethodVisitor 是一個抽象類瞬逊,當 ASM 的 ClassReader 讀取到 Method 時就轉(zhuǎn)入 MethodVisitor 接口處理。AdviceAdapter 是 MethodVisitor 的子類仪或,使用 AdviceAdapter 可以更方便的修改方法的字節(jié)碼确镊。AdviceAdapter其中幾個重要方法如下:
void visitCode()
:表示 ASM 開始掃描這個方法
void onMethodEnter()
:進入這個方法
void onMethodExit()
:即將從這個方法出去
void onVisitEnd()
:表示方法掃描完畢
我們來重點看下ClassVisitor
類
ClassVisitor
類的API如下
4.1 visit
/**
* 可以拿到類的詳細信息
*
* @param version jdk的版本: 52 代表jdk版本 1.8;51 代表jdk版本 1.7
* @param access 類的修飾符:ACC_PUBLIC范删、ACC_PRIVATE蕾域、ACC_PROTECTED、ACC_FINAL到旦、ACC_SUPER
* @param name 類的名稱:以路徑的形式表示 com/joker/demo/TestClass
* @param signature 泛型信息:未定義泛型旨巷,則該參數(shù)為null
* @param superName 表示當前類所繼承的父類
* @param interfaces 表示類所實現(xiàn)的接口列表
*/
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
}
類的修飾符
類的修飾符以“ACC_開頭”,可以作用到類級別上的修飾符主要有下面這些
修飾符 | 含義 |
---|---|
ACC_PUBLIC | public |
ACC_PRIVATE | private |
ACC_PROTECTED | protected |
ACC_FINAL | final |
ACC_SUPER | extends |
ACC_INTERFACE | 接口 |
ACC_ABSTRACT | 抽象類 |
ACC_ANNOTATION | 注解類型 |
ACC_ENUM | 枚舉類型 |
ACC_DEPRECATED | 標記了@Deprecated注解的類 |
ACC_SYNTHETIC | javac生成 |
4.2 visitAnnotation
/**
* 當掃描器掃描到類注解聲明時進行調(diào)用
*
* @param desc 注解類型(簽名類型)
* @param visible 注解是否可以在 JVM 中可見
* @return
*/
@Override
AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return super.visitAnnotation(desc, visible)
}
4.3 visitField
/**
* 當掃描器掃描到類中字段時進行調(diào)用
*
* @param access 修飾符
* @param name 字段名
* @param desc 字段類型
* @param signature 泛型描述
* @param value 默認值
* @return
*/
@Override
FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
return super.visitField(access, name, desc, signature, value)
}
4.4 visitMethod
/**
* 當掃描器掃描到類的方法時調(diào)用
*
* @param access 方法的修飾符
* @param name 方法名
* @param desc 方法簽名
* @param signature 表示泛型相關(guān)的信息
* @param exceptions 表示將會拋出的異常添忘,如果方法沒有拋出異常采呐,則參數(shù)為空
* @return
*/
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return super.visitMethod(access, name, desc, signature, exceptions)
}
方法的修飾符
可以作用到方法級別上的修飾符主要有下面這些
修飾符 | 含義 |
---|---|
ACC_PUBLIC | public |
ACC_PRIVATE | private |
ACC_PROTECTED | protected |
ACC_STATIC | static |
ACC_FINAL | final |
ACC_SYNCHRONIZED | 同步的 |
ACC_VARARGS | 不定參數(shù)個數(shù)的方法 |
ACC_NATIVE | native類型方法 |
ACC_ABSTRACT | 抽象的方法 |
ACC_DEPRECATED | 標記了@Deprecated注解的類 |
ACC_SYNTHETIC | javac生成 |
方法的簽名格式
(參數(shù)列表)返回值類型
在ASM中不同的類型對應(yīng)不同的代碼,詳細的對應(yīng)關(guān)系如下表
代碼 | 類型 |
---|---|
I | int |
B | byte |
C | char |
D | double |
F | float |
J | long |
S | short |
Z | boolean |
V | void |
[...; | 數(shù)組 |
[[...; | 二維數(shù)組 |
[[[...; | 三維數(shù)組 |
方法參數(shù)列表對應(yīng)的方法簽名示例如下
參數(shù)列表 | 方法參數(shù) |
---|---|
String[] | [Ljava/lang/String; |
String[][] | [[Ljava/lang/String; |
int搁骑,String斧吐,String[] | ILjava/lang/String;[Ljava/lang/String; |
int,boolean仲器,long煤率,String[],double | IZJ[Ljava/lang/String;D |
Class<?>, String, Object...paramType | Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object; |
int[] | [I |
4.5 visitEnd
/**
* 當掃描器完成類掃描時才會調(diào)用
*/
@Override
void visitEnd() {
super.visitEnd()
}
5乏冀、ASM練手demo實現(xiàn)統(tǒng)計方法時長代碼插樁
5.1添加ASM依賴
implementation 'org.ow2.asm:asm-all:5.2'
5.2定義一個HelloWorld類
public class HelloWorld {
public void sayHello() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.3通過javac
命令執(zhí)行HelloWorld.java
得到HelloWorld.class
目前桌面上已經(jīng)生成了HelloWorld.class
字節(jié)碼文件
5.4新建一個ASMTest類蝶糯,從桌面讀取HelloWorld.class
文件,通過ASM讀取HelloWorld.class
文件辆沦,并將打印sayHello()
方法調(diào)用時長的代碼插樁到sayHello()
方法中昼捍,輸出新的字節(jié)碼文件OutputHelloWorld.class
到桌面
public class ASMTest {
public static void redefineHelloWorldClass() {
try {
InputStream inputStream = new FileInputStream("/Users/jokerwan/Desktop/HelloWorld.class");
// 1. 創(chuàng)建 ClassReader 讀入 .class 文件到內(nèi)存中
ClassReader reader = new ClassReader(inputStream);
// 2. 創(chuàng)建 ClassWriter 對象,將操作之后的字節(jié)碼的字節(jié)數(shù)組回寫
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
// 3. 創(chuàng)建自定義的 ClassVisitor 對象
ClassVisitor change = new ChangeVisitor(writer);
// 4. 將 ClassVisitor 對象傳入 ClassReader 中
reader.accept(change, ClassReader.EXPAND_FRAMES);
System.out.println("Success!");
// 獲取修改后的 class 文件對應(yīng)的字節(jié)數(shù)組
byte[] code = writer.toByteArray();
try {
// 將二進制流寫到本地磁盤上
FileOutputStream fos = new FileOutputStream("/Users/jokerwan/Desktop/OutputHelloWorld.class");
fos.write(code);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("Failure!");
}
}
static class ChangeVisitor extends ClassVisitor {
ChangeVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("<init>")) {
return methodVisitor;
}
return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
}
}
static class ChangeAdapter extends AdviceAdapter {
private int startTimeId = -1;
private String methodName = null;
ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
methodName = name;
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
startTimeId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitIntInsn(LSTORE, startTimeId);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
int durationId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, startTimeId);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, durationId);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("The cost time of " + methodName + "() is ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, durationId);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" ms");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
5.5通過單元測試執(zhí)行ASMTest.redefineHelloWorldClass();
public class ExampleUnitTest {
@Test
public void testASM() {
ASMTest.redefineHelloWorldClass();
}
}
OutputHelloWorld.class
已經(jīng)輸出到桌面
將OutputHelloWorld.class
拖到Android Studio中众辨,Android Studio會將字節(jié)碼文件反編譯為java文件端三,反編譯后的代碼如下
可以看到我們成功通過ASM將統(tǒng)計運行時長的代碼插入到sayHello()
方法中。
demo代碼如下
https://github.com/isJoker/ASM_Demo
參考文章
https://asm.ow2.io/developer-guide.html#classreader
https://www.ibm.com/developerworks/cn/java/j-lo-asm30/