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();
}
  1. void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
    該方法是當掃描類時第一個調用的方法,主要用于類聲明使用。下面是對方法中各個參數的示意:visit( 類版本 , 修飾符 , 類名 , 泛型信息 , 繼承的父類 , 實現的接口)
  2. AnnotationVisitor visitAnnotation(String desc, boolean visible)
    該方法是當掃描器掃描到類注解聲明時進行調用。下面是對方法中各個參數的示意:visitAnnotation(注解類型 , 注解是否可以在 JVM 中可見)。
  3. FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
    該方法是當掃描器掃描到類中字段時進行調用。下面是對方法中各個參數的示意:visitField(修飾符 , 字段名 , 字段類型 , 泛型描述 , 默認值)
  4. MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    該方法是當掃描器掃描到類的方法時進行調用。下面是對方法中各個參數的示意:visitMethod(修飾符 , 方法名 , 方法簽名 , 泛型信息 , 拋出的異常)
  5. 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

其中比較重要的幾個方法如下:

  1. void visitCode():表示 ASM 開始掃描這個方法
  2. void onMethodEnter():進入這個方法
  3. void onMethodExit():即將從這個方法出去
  4. void onVisitEnd():表示方法掃碼完畢

2.5 FieldVisitor 抽象類

FieldVisitor 是一個抽象類,當 ASM 的 ClassReader 讀取到 Field 時就轉入 FieldVisitor 接口處理。和分析 MethodVisitor 的方法一樣,也可以查看源碼注釋進行學習,這里不再詳細介紹

2.6 操作流程

  1. 需要創建一個 ClassReader 對象,將 .class 文件的內容讀入到一個字節數組中
  2. 然后需要一個 ClassWriter 的對象將操作之后的字節碼的字節數組回寫
  3. 需要事件過濾器 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.javajavap -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);
        }
    }
}

執行結果如下圖所示


Class.png

反編譯 HelloWorld2.class 文件的內容如下所示


Class1.png

3.2 修改類中屬性的字節碼

這一節中我們將展示一下如何使用 Core API 對類中的屬性進行操作。

假如說,現在有一個 Person.java 類如下所示:

public class Person {
    public String name;
    public int sex;
}

我們想為這個類,添加一個 ‘public int age’ 的屬性該怎么添加呢?我們會面對兩個問題:

  1. 該調用 ASM 的哪個 API 添加屬性呢?
  2. 在何時寫添加屬性的代碼?

接下來,我們就一一解決上面的兩個問題?

3.2.1 添加屬性的 API

按照我們分析的上述的 2.6 操作流程敘述,需要以下三個步驟:

  1. 需要創建一個 ClassReader 對象,將 .class 文件的內容讀入到一個字節數組中
  2. 然后需要一個 ClassWriter 的對象將操作之后的字節碼的字節數組回寫
  3. 需要創建一個事件過濾器 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() 方法中是添加屬性的最佳時機

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 自動生成:

  1. 通過 javac 編譯該 Demo.java 文件生成對應的 Demo.class 文件,如下所示
javac Demo.java
  1. 通過 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 

截圖如下:


DemoDump.png

深入字節碼 -- 玩轉 ASM-Bytecode 原 薦
美團熱更方案ASM實踐

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 引言 什么是 ASM ? ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。AS...
    Chauncey_Chen閱讀 1,505評論 0 6
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • 前言 很早之前就寫過面向切面的編程思想,主要學習了AOP的思想(參考:AOP簡介)以及使用 AspectJ 實現簡...
    Whyn閱讀 10,877評論 4 40
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,123評論 6 13
  • 自我覺察: 語言 具體事項: 奶奶重病,爺爺總是找爸爸看護,但家里有5個孩子,有很長一段時間卻只讓爸爸白班沒人輪流...
    馨新閱讀 503評論 0 0