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

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 類的派生類可以僅封裝單一功能,比如刪除某函數、修改字段可見性等等,然後再加入到職責鏈中,這樣耦合更小,重用的概率也更大,但代價是產生很多小對象,而且職責鏈的層次太長的話也會加大系統調用的開銷,用戶需要在低耦合和高效率之間作出權衡。

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