【趣味設計模式系列】之【代理模式4--ASM框架解析】

小豬爸爸發表於2020-09-02

1. 簡介

ASM是assemble英文的簡稱,中文名為彙編,官方地址https://asm.ow2.io/,下面是官方的一段英文簡介:

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

ASM is used in many projects, including:

  • the OpenJDK, to generate the lambda call sites, and also in the Nashorn compiler,
  • the Groovy compiler and the Kotlin compiler,
  • Cobertura and Jacoco, to instrument classes in order to measure code coverage,
  • CGLIB, to dynamically generate proxy classes (which are used in other projects such as Mockito and EasyMock),
  • Gradle, to generate some classes at runtime.

翻譯如下:
ASM是一個通用的Java位元組碼操作和分析框架。它可用於修改現有的類或直接以二進位制形式動態生成類。ASM提供了一些常見的位元組碼轉換和分析演算法,從中可以構建定製的複雜轉換和程式碼分析工具。ASM提供了與其他Java位元組碼框架類似的功能,但側重於效能。因為它被設計和實現得儘可能的小和快,所以它非常適合在動態系統中使用(當然也可以以靜態的方式使用,例如在編譯器中)。
主要用途:

  • OpenJDK,用來生成lambda呼叫站點,還有在Nashorn編譯器中,
  • Groovy編譯器和Kotlin編譯器,
  • Cobertura和Jacoco,用來測量程式碼覆蓋率,
  • CGLIB為了動態生成代理類(在其他專案中使用,如mock和EasyMock),
  • Gradle在執行時生成一些類。

2. 框架使用

官方提供了使用手冊,地址:https://asm.ow2.io/asm4-guide.pdf,引入依賴

<dependency>
      <groupId>asm</groupId>
      <artifactId>asm-all</artifactId>
      <version>3.3.1</version>
</dependency>

下面結合例子分析

2.1 ClassReader--解析一個類檔案

建立一個T1類

package com.wzj.asm;

/**
 * 游標必須位於類體內,View-Show ByteCode
 */

public class T1 {
    int i = 0;
    public void m() {
        int j=1;
    }

}

在idea中的安裝外掛ByteCode外掛

然後通過idea中的View選單->show bytecode看到位元組碼檔案


下面的ClassPrinter類用來實現解析T1.Class這個類

package com.wzj.asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

import java.io.IOException;

import static org.objectweb.asm.Opcodes.ASM4;

/**
 * @Author: wzj
 * @Date: 2020/8/5 21:29
 * @Desc: 解析一個類
 */
public class ClassPrinter extends ClassVisitor {
    public ClassPrinter() {
        super(ASM4);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + "{" );
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        System.out.println("    " + name);
        return null;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("    " + name + "()");
        return null;
    }

    @Override
    public void visitEnd() {

        System.out.println("}");
    }

    public static void main(String[] args) throws IOException {
        ClassPrinter cp = new ClassPrinter();
        ClassReader cr = new ClassReader(
                ClassPrinter.class.getClassLoader().getResourceAsStream("com/wzj/asm/T1.class"));


        cr.accept(cp, 0);
    }
}

visit方法訪問類的類名、父類等資訊,visitField方法訪問類的屬性資訊,visitMethod方法訪問類的方法資訊,最後列印出該類的資訊

2.2 ClassWriter--生成一個類檔案

package com.wzj.asm;

import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;

import static org.objectweb.asm.Opcodes.*;

/**
 * @Author: wzj
 * @Date: 2020/8/5 21:26
 * @Desc: 生成一個類
 */
public class ClassWriterTest {
    public static void main(String[] args) throws Exception {
        ClassWriter cw = new ClassWriter(0);
        cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
                "pkg/Comparable", null, "java/lang/Object",
                null);
        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
                null, -1).visitEnd();
        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
                null, 0).visitEnd();
        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
                null, 1).visitEnd();
        cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
                "(Ljava/lang/Object;)I", null, null).visitEnd();
        cw.visitEnd();
        byte[] b = cw.toByteArray();

        MyClassLoader myClassLoader = new MyClassLoader();
        Class c = myClassLoader.defineClass("pkg.Comparable", b);
        System.out.println(c.getMethods()[0].getName());

        String path = (String)System.getProperties().get("user.dir");
        File f = new File(path + "/com/wzj/asm/");
        f.mkdirs();
        FileOutputStream fos = new FileOutputStream(new File(path + "/com/wzj/asm/Comparable.class"));
        fos.write(b);
    }
}

自定義一個類載入器

package com.wzj.asm;

/**
 * @Author: wzj
 * @Date: 2020/8/5 21:26
 * @Desc: 自定義類載入器
 */
public class MyClassLoader extends ClassLoader{
    public Class defineClass(String name, byte[] b) {
        return defineClass(name, b, 0, b.length);
    }
}

生成的類檔案如下

package pkg;

public interface Comparable {
    int LESS = -1;
    int EQUAL = 0;
    int GREATER = 1;

    int compareTo(Object var1);
}

2.3 利用ClassVisitor對原始類方法增強功能

依然使用之前代理系列的例子Apple類

package com.wzj.asm;

import com.wzj.proxy.v8.Sellalbe;

import java.util.Random;

/**
 * @Author: wzj
 * @Date: 2020/8/3 10:29
 * @Desc: 待銷蘋果
 */
public class Apple implements Sellalbe {

    @Override
    public void secKill() {
        System.out.println("蘋果正在秒殺中...");
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ClassTransformedTest類,內部類ClassVisitor實現父類的方法visitMethod,並在方法中判斷目標方法為secKill時,對該方法增加TimeProxy.before()方法,程式碼如下

package com.wzj.asm;

import org.objectweb.asm.*;

import java.io.File;
import java.io.FileOutputStream;

import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;


/**
 * @Author: wzj
 * @Date: 2020/9/1 21:10
 * @Desc: 類方法增強
 */
public class ClassTransformedTest {
    public static void main(String[] args) throws Exception {
        ClassReader cr = new ClassReader(
                ClassPrinter.class.getClassLoader().getResourceAsStream("com/wzj/asm/Apple.class"));

        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new ClassVisitor(ASM4, cw) {
            //增強secKill方法, 在方法里加入時間代理
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
                return new MethodVisitor(ASM4, mv) {
                    @Override
                    public void visitCode() {
                        if(name.equals("secKill")) {
                            visitMethodInsn(INVOKESTATIC, "com/wzj/asm/TimeProxy","before", "()V", false);
                            super.visitCode();
                        }
                    }
                };
            }
        };


        cr.accept(cv, 0);
        byte[] b2 = cw.toByteArray();

        MyClassLoader cl = new MyClassLoader();
        Class c2 = cl.defineClass("com.wzj.asm.Apple", b2);
        c2.getConstructor().newInstance();


        String path = (String)System.getProperties().get("user.dir");
        File f = new File(path + "/com/wzj/asm/");

        if(!f.exists()) {
            f.mkdirs();
        }

        FileOutputStream fos = new FileOutputStream(new File(path + "/com/wzj/asm/Apple.class"));
        fos.write(b2);
        fos.flush();
        fos.close();

    }
}

最終生成的增強類如下

package com.wzj.asm;

import com.wzj.proxy.v8.Sellalbe;
import java.util.Random;

public class Apple implements Sellalbe {
    public Apple() {
    }

    public void secKill() {
        TimeProxy.before();
        System.out.println("蘋果正在秒殺中...");

        try {
            Thread.sleep((long)(new Random()).nextInt(3000));
        } catch (InterruptedException var2) {
            var2.printStackTrace();
        }

    }
}

3. 框架剖析

ASM的核心類是ClassVisitor、MethodVisitor,通過訪問者模式,對位元組碼檔案類的資訊、方法的資訊進行增加或者修改,因為對於一個java的class類,它的結構是完全固定的,包括大致10幾項,分別為Magic(魔數)、Version(版本)、Constant Pool(常量池)、Access_flag(訪問標識)、This Class(本例項指標)、Super Class(父類例項指標)、Interfaces(介面)、Fields(欄位)、Methods(方法)、Class attributes(類屬性)等。

在 ASM 中,ClassReader 類,它能正確的分析位元組碼,構建出抽象的樹在記憶體中表示位元組碼。它會呼叫 accept 方法,這個方法接受一個實現了 ClassVisitor 介面的物件例項作為引數,然後依次呼叫 ClassVisitor 介面的各個方法。位元組碼空間上的偏移被轉換成 visit 事件時間上呼叫的先後,所謂 visit 事件是指對各種不同 visit 函式的呼叫, ClassReader 知道如何呼叫各種 visit 函式。在這個過程中使用者無法對操作進行干涉,所以遍歷的演算法是確定的,使用者可以做的是提供不同的 Visitor 來對位元組碼樹進行不同的修改。

ClassVisitor 會產生一些子過程,比如 visitMethod 會返回一個實現 MethordVisitor 介面的例項, visitField 會返回一個實現 FieldVisitor 介面的例項,完成子過程後控制返回到父過程,繼續訪問下一節點。因此對於 ClassReader 來說,其內部順序訪問是有一定要求的。實際上使用者還可以不通過 ClassReader 類,自行手工控制這個流程,只要按照一定的順序,各個 visit 事件被先後正確的呼叫,最後就能生成可以被正確載入的位元組碼。當然獲得更大靈活性的同時也加大了調整位元組碼的複雜度。

各個 ClassVisitor 通過職責鏈 (Chain-of-responsibility) 模式,可以非常簡單的封裝對位元組碼的各種修改,而無須關注位元組碼的位元組偏移,因為這些實現細節對於使用者都被隱藏了,使用者要做的只是覆寫相應的 visit 函式。官方給出瞭如下圖說明通過責任鏈修改類,分別代表簡單責任鏈與複雜責任鏈下各種的應用

ClassAdaptor 類實現了 ClassVisitor 介面所定義的所有函式,當新建一個 ClassAdaptor 物件的時候,需要傳入一個實現了 ClassVisitor 介面的物件,作為職責鏈中的下一個訪問者 (Visitor),這些函式的預設實現就是簡單的把呼叫委派給這個物件,然後依次傳遞下去形成職責鏈。當使用者需要對位元組碼進行調整時,只需從 ClassAdaptor 類派生出一個子類,覆寫需要修改的方法,完成相應功能後再把呼叫傳遞下去。這樣,使用者無需考慮位元組偏移,就可以很方便的控制位元組碼。官方也給出了一個介面卡模式的圖

每個 ClassAdaptor 類的派生類可以僅封裝單一功能,比如刪除某函式、修改欄位可見性等等,然後再加入到職責鏈中,這樣耦合更小,重用的概率也更大,但代價是產生很多小物件,而且職責鏈的層次太長的話也會加大系統呼叫的開銷,使用者需要在低耦合和高效率之間作出權衡。

相關文章