深入開源框架底層之ASM

什麼是 ASM ?

ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,能夠改變類行爲,分析類信息,甚至能夠根據用戶要求生成新類。

爲什麼要動態生成 Java 類?

想象一下,如果開源框架要求你添加各種Java類來實現諸如log、cache、transaction等功能,我想這個開源框架你肯定不會用吧。動態生成類可以減少對你代碼的侵入,提高使用者的效率。

爲什麼選擇ASM?

最直接的改造 Java 類的方法莫過於直接改寫 class 文件。Java 規範詳細說明了 class 文件的格式,直接編輯字節碼確實可以改變 Java 類的行爲。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 文件動手術。是的,這是最直接的方法,但是要求使用者對 Java class 文件的格式了熟於心:小心地推算出想改造的函數相對文件首部的偏移量,同時重新計算 class 文件的校驗碼以通過 Java 虛擬機的安全機制。

可以發現,直接操作class文件是比較麻煩的,就跟爲什麼我們都選擇使用框架一樣,框架屏蔽了底層的複雜性。ASM就是操作class的一把利器。

使用 ASM 編程

ASM提供了兩種API:

  1. CoreAPI(ClassVisitor 、MethodVisitor等)
  2. TreeAPI(ClassNode,MethodNode等)

區別是CoreAPI基於事件模型,定義了Class中各個元素的Visitor,不需要加載整個Class到內存中。而TreeAPI以Tree結構將Class整個結構讀取到內存中。從使用角度來說TreeAPI更爲簡單。

以下示例採用的是CoreAPI方式。

添加Maven:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>5.0.4</version>
</dependency>

使用的相對比較穩定,使用比較多的版本5.0.4。

首先說明一下,修改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。

ASM Bytecode Outline 2017

但是那麼多指令、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

參考資料:

https://asm.ow2.io/

https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html

https://juejin.im/post/5b549bcbe51d45169c1c8b66

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章