深刻開源框架底層之ASM

什麼是 ASM ?

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

爲何要動態生成 Java 類?

想象一下,若是開源框架要求你添加各類Java類來實現諸如log、cache、transaction等功能,我想這個開源框架你確定不會用吧。動態生成類能夠減小對你代碼的侵入,提升使用者的效率。java

爲何選擇ASM?

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

能夠發現,直接操做class文件是比較麻煩的,就跟爲何咱們都選擇使用框架同樣,框架屏蔽了底層的複雜性。ASM就是操做class的一把利器。github

使用 ASM 編程

ASM提供了兩種API:編程

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

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

如下示例採用的是CoreAPI方式。安全

添加Maven:bash

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>5.0.4</version>
</dependency>
複製代碼

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

首先說明一下,修改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,你將會看到以下視圖:

image.png

切換的ASMified視圖,你會看到跟咱們上面寫的同樣的代碼,直接Copy過來使用便可。

查看示例完整源代碼:asm_demo

參考資料:

asm.ow2.io/

www.ibm.com/developerwo…

juejin.im/post/5b549b…

相關文章
相關標籤/搜索