ASM 庫的介紹和使用
前面幾篇文章介紹了 .class 檔案的結構、JVM 如何載入 .class 檔案、JVM 中如何執行方法的呼叫已經訪問者模式,其實前面幾篇文章都是為這篇文章做鋪墊的,如果不知道 .class 檔案結構、也不知道在 JVM 中,.class 檔案中的方法是如何被執行的,這篇文章中的有些部分可能會看不懂,所以推薦先看下前面幾篇文章。
這篇文章主要介紹 ASM 庫的結構、主要的 API,並且通過兩個示例說明如何通過 ASM 修改 .class 檔案中的方法和屬性。

catalog.png
一. ASM 的結構
ASM 庫是一款基於 Java 位元組碼層面的程式碼分析和修改工具。ASM 可以直接生產二進位制的 class 檔案,也可以在類被載入入 JVM 之前動態修改類行為。
ASM 庫的結構如下所示:

asm_arch.png
- 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 的方法如下所示:

AdviceAdapter.png
其中比較重要的幾個方法如下:
- 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: goto14 9: astore_1 10: aload_1 11: invokevirtual #6// Method java/lang/InterruptedException.printStackTrace:()V 14: return Exception table: fromtotarget type 069Class 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); } } }
執行結果如下圖所示

Class.png
反編譯 HelloWorld2.class 檔案的內容如下所示

Class1.png
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);

visitField.png
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(); } } }
其輸出入下所示:

visitFieldResult.png
那如果我們嘗試在 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); } }
還是使用上面的測試程式碼測試一下,會有如下的測試結果

visitFieldError.png
在 Person 類中有重複的屬性,為什麼會報這個錯誤呢?
分析 ClassVisitor#visitField() 方法可得知,只要訪問類中的一個屬性,visitField() 方法就會被呼叫一次,在 Person 類中有兩個屬性,所以 visitField() 方法就會被呼叫兩次,也就添加了兩次 ‘public int age’ 屬性,就報了上述的錯誤,而 visitEnd() 方法只有在最後才會被呼叫且只調用一次,所以在 visitEnd() 方法中是新增屬性的最佳時機