前面幾篇文章介紹了 .class 文件的結構、JVM 如何加載 .class 文件、JVM 中如何執行方法的調用和訪問者模式,其實前面幾篇文章都是為這篇文章做鋪墊的,如果不知道 .class 文件結構、也不知道在 JVM 中 .class 文件中的方法是如何被執行的,這篇文章中的有些部分可能會看不懂,所以推薦先看下前面幾篇文章。
這篇文章主要介紹 ASM 庫的結構、主要的 API,并且通過兩個示例說明如何通過 ASM 修改 .class 文件中的方法和屬性。
一. ASM 的結構
ASM 庫是一款基于 Java 字節碼層面的代碼分析和修改工具。ASM 可以直接生產二進制的 class 文件,也可以在類被加載入 JVM 之前動態修改類行為。
ASM 庫的結構如下所示:
- Core:為其他包提供基礎的讀、寫、轉化Java字節碼和定義的API,并且可以生成Java字節碼和實現大部分字節碼的轉換,在 訪問者模式和 ASM 中介紹的幾個重要的類就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 類.
- Tree:提供了 Java 字節碼在內存中的表現
- Commons:提供了一些常用的簡化字節碼生成、轉換的類和適配器
- Util:包含一些幫助類和簡單的字節碼修改類,有利于在開發或者測試中使用
- XML:提供一個適配器將XML和SAX-comliant轉化成字節碼結構,可以允許使用XSLT去定義字節碼轉化
二. Core API 介紹
2.1 ClassVisitor 抽象類
如下所示,在 ClassVisitor 中提供了和類結構同名的一些方法,這些方法會對類中相應的部分進行操作,而且是有順序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd
public abstract class ClassVisitor {
......
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
public AnnotationVisitor visitAnnotation(String desc, boolean visible);
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
public void visitEnd();
}
- void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
該方法是當掃描類時第一個調用的方法,主要用于類聲明使用。下面是對方法中各個參數的示意:visit( 類版本 , 修飾符 , 類名 , 泛型信息 , 繼承的父類 , 實現的接口) - AnnotationVisitor visitAnnotation(String desc, boolean visible)
該方法是當掃描器掃描到類注解聲明時進行調用。下面是對方法中各個參數的示意:visitAnnotation(注解類型 , 注解是否可以在 JVM 中可見)。 - FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
該方法是當掃描器掃描到類中字段時進行調用。下面是對方法中各個參數的示意:visitField(修飾符 , 字段名 , 字段類型 , 泛型描述 , 默認值) - MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
該方法是當掃描器掃描到類的方法時進行調用。下面是對方法中各個參數的示意:visitMethod(修飾符 , 方法名 , 方法簽名 , 泛型信息 , 拋出的異常) - void visitEnd()
該方法是當掃描器完成類掃描時才會調用,如果想在類中追加某些方法
2.2 ClassReader 類
這個類會將 .class 文件讀入到 ClassReader 中的字節數組中,它的 accept 方法接受一個 ClassVisitor 實現類,并按照順序調用 ClassVisitor 中的方法
2.3 ClassWriter 類
ClassWriter 是一個 ClassVisitor 的子類,是和 ClassReader 對應的類,ClassReader 是將 .class 文件讀入到一個字節數組中,ClassWriter 是將修改后的類的字節碼內容以字節數組的形式輸出。
2.4 MethodVisitor & AdviceAdapter
MethodVisitor 是一個抽象類,當 ASM 的 ClassReader 讀取到 Method 時就轉入 MethodVisitor 接口處理。
AdviceAdapter 是 MethodVisitor 的子類,使用 AdviceAdapter 可以更方便的修改方法的字節碼。
AdviceAdapter 的方法如下所示:
其中比較重要的幾個方法如下:
- void visitCode():表示 ASM 開始掃描這個方法
- void onMethodEnter():進入這個方法
- void onMethodExit():即將從這個方法出去
- void onVisitEnd():表示方法掃碼完畢
2.5 FieldVisitor 抽象類
FieldVisitor 是一個抽象類,當 ASM 的 ClassReader 讀取到 Field 時就轉入 FieldVisitor 接口處理。和分析 MethodVisitor 的方法一樣,也可以查看源碼注釋進行學習,這里不再詳細介紹
2.6 操作流程
- 需要創建一個 ClassReader 對象,將 .class 文件的內容讀入到一個字節數組中
- 然后需要一個 ClassWriter 的對象將操作之后的字節碼的字節數組回寫
- 需要事件過濾器 ClassVisitor。在調用 ClassVisitor 的某些方法時會產生一個新的 XXXVisitor 對象,當我們需要修改對應的內容時只要實現自己的 XXXVisitor 并返回就可以了
三. 示例
3.1 修改類中方法的字節碼
假如現在我們有一個 HelloWorld 類,如下
package com.lijiankun24.asmpractice.demo;
public class HelloWorld {
public void sayHello() {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通過 javac HelloWorld.java
和 javap -verbose HelloWorld.class
可以查看到 sayName() 方法的字節碼如下所示:
public void sayHello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: ldc2_w #2 // long 2000l
3: invokestatic #4 // Method java/lang/Thread.sleep:(J)V
6: goto 14
9: astore_1
10: aload_1
11: invokevirtual #6 // Method java/lang/InterruptedException.printStackTrace:()V
14: return
Exception table:
from to target type
0 6 9 Class java/lang/InterruptedException
LineNumberTable:
line 5: 0
line 8: 6
line 6: 9
line 7: 10
line 9: 14
StackMapTable: number_of_entries = 2
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
我們通過 ASM 修改 HelloWorld.class 字節碼文件,實現統計方法執行時間的功能
public class CostTime {
public static void main(String[] args) {
redefinePersonClass();
}
private static void redefinePersonClass() {
String className = "com.lijiankun24.asmpractice.demo.HelloWorld";
try {
InputStream inputStream = new FileInputStream("/Users/lijiankun/Desktop/HelloWorld.class");
ClassReader reader = new ClassReader(inputStream); // 1. 創建 ClassReader 讀入 .class 文件到內存中
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); // 2. 創建 ClassWriter 對象,將操作之后的字節碼的字節數組回寫
ClassVisitor change = new ChangeVisitor(writer); // 3. 創建自定義的 ClassVisitor 對象
reader.accept(change, ClassReader.EXPAND_FRAMES); // 4. 將 ClassVisitor 對象傳入 ClassReader 中
Class clazz = new MyClassLoader().defineClass(className, writer.toByteArray());
Object personObj = clazz.newInstance();
Method nameMethod = clazz.getDeclaredMethod("sayHello", null);
nameMethod.invoke(personObj, null);
System.out.println("Success!");
byte[] code = writer.toByteArray(); // 獲取修改后的 class 文件對應的字節數組
try {
FileOutputStream fos = new FileOutputStream("/Users/lijiankun/Desktop/HelloWorld2.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.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
執行結果如下圖所示
反編譯 HelloWorld2.class 文件的內容如下所示
3.2 修改類中屬性的字節碼
這一節中我們將展示一下如何使用 Core API 對類中的屬性進行操作。
假如說,現在有一個 Person.java 類如下所示:
public class Person {
public String name;
public int sex;
}
我們想為這個類,添加一個 ‘public int age’ 的屬性該怎么添加呢?我們會面對兩個問題:
- 該調用 ASM 的哪個 API 添加屬性呢?
- 在何時寫添加屬性的代碼?
接下來,我們就一一解決上面的兩個問題?
3.2.1 添加屬性的 API
按照我們分析的上述的 2.6 操作流程敘述,需要以下三個步驟:
- 需要創建一個 ClassReader 對象,將 .class 文件的內容讀入到一個字節數組中
- 然后需要一個 ClassWriter 的對象將操作之后的字節碼的字節數組回寫
- 需要創建一個事件過濾器 ClassVisitor。事件過濾器中的某些方法可以產生一個新的XXXVisitor對象,當我們需要修改對應的內容時只要實現自己的XXXVisitor并返回就可以了
在上面三個步驟中,可以操作的就是 ClassVisitor 了。ClassVisitor 接口提供了和類結構同名的一些方法,這些方法可以對相應的類結構進行操作。
在使用 ClassVisitor 添加類屬性的時候,只需要添加一句話就可以了:
classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
3.2.2 添加屬性的時機
我們先暫且在 ClassVisitor 的 visitEnd() 方法中寫入上面的代碼,如下所示
public class Transform extends ClassVisitor {
public Transform(ClassVisitor cv) {
super(cv);
}
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
}
}
我們寫如下的測試類,測試一下
public class FieldPractice {
public static void main(String[] args) {
addAgeField();
}
private static void addAgeField() {
try {
InputStream inputStream = new FileInputStream("/Users/lijiankun/Desktop/Person.class");
ClassReader reader = new ClassReader(inputStream);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new Transform(writer);
reader.accept(visitor, ClassReader.SKIP_DEBUG);
byte[] classFile = writer.toByteArray();
MyClassLoader classLoader = new MyClassLoader();
Class clazz = classLoader.defineClass("Person", classFile);
Object obj = clazz.newInstance();
System.out.println(clazz.getDeclaredField("name").get(obj)); //----(1)
System.out.println(clazz.getDeclaredField("age").get(obj)); //----(2)
} catch (Exception e) {
e.printStackTrace();
}
}
}
其輸出入下所示:
那如果我們嘗試在 ClassVisitor#visitField() 方法中添加屬性可以嗎?我們可以修改 Transform 測試一下:
public class Transform extends ClassVisitor {
Transform(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
return super.visitField(access, name, desc, signature, value);
}
}
還是使用上面的測試代碼測試一下,會有如下的測試結果
在 Person 類中有重復的屬性,為什么會報這個錯誤呢?
分析 ClassVisitor#visitField() 方法可得知,只要訪問類中的一個屬性,visitField() 方法就會被調用一次,在 Person 類中有兩個屬性,所以 visitField() 方法就會被調用兩次,也就添加了兩次 ‘public int age’ 屬性,就報了上述的錯誤,而 visitEnd() 方法只有在最后才會被調用且只調用一次,所以在 visitEnd() 方法中是添加屬性的最佳時機
3.3 ASMifier
可能有人會問,我剛開始學,上面例子中那些 ASM 的代碼我還不會寫,怎么辦呢?ASM 官方為我們提供了 ASMifier,可以幫助我們生成這些晦澀難懂的 ASM 代碼。
比如,我想通過 ASM 實現統計一個方法的執行時間,該怎么做呢?一般會有如下的代碼:
package com.lijiankun24.classpractice;
public class Demo {
public void costTime() {
long startTime = System.currentTimeMillis();
// ......
long duration = System.currentTimeMillis() - startTime;
System.out.println("The cost time of this method is " + duration + " ms");
}
}
那上面這段代碼對應的 ASM 代碼是什么呢?我們可以通過以下兩個步驟,使用 ASMifier 自動生成:
- 通過
javac
編譯該Demo.java
文件生成對應的Demo.class
文件,如下所示
javac Demo.java
- 通過 ASMifier 自動生成對應的 ASM 代碼。首先需要在ASM官網 下載
asm-all.jar
庫,我下載的是最新的asm-all-5.2.jar
,然后使用如下命令,即可生成
java -classpath asm-all-5.2.jar org.objectweb.asm.util.ASMifier Demo.class
截圖如下: