開啟java語言世界通往位元組碼世界的大門——ASM位元組碼操作類庫

發表於2024-02-12

一、ASM介紹

1、ASM 是什麼

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

一個.java檔案經過Java編譯器(javac)編譯之後會生成一個.class檔案。在.class檔案中,儲存的是位元組碼(ByteCode)資料。ASM所操作的物件是位元組碼(ByteCode),而在許多情況下,位元組碼(ByteCode)的具體表現形式是.class檔案。

ASM處理位元組碼(ByteCode)的方式是“拆分-修改-合併”。

位元組碼工具類建立實現介面方法呼叫類擴充套件父類方法呼叫優點缺點常見使用學習成本
java-proxy支援支援支援不支援不支援簡單動態代理首選功能有限,不支援擴充套件spring-aop,MyBatis1星
asm支援支援支援支援支援任意位元組碼插入,幾乎不受限制學習難度大,編寫程式碼多cglib5星
javaassit支援支援支援支援支援java原始語法,字串形式插入,寫入直觀不支援jdk1.5以上的語法,如泛型,增強forFastjson,MyBatis2星
cglib支援支援支援支援支援與bytebuddy看起來差不多正在被bytebuddy淘汰EasyMock,jackson-databind3星
bytebuddy支援支援支援支援支援支援任意維度的攔截,可以獲取原始類、方法,以及代理類和全部引數不太直觀,學習理解有些成本,API非常多SkyWalking,Mockito,Hibernate,powermock3星

•ASM官網:https://asm.ow2.io/

•ASM原始碼:https://gitlab.ow2.org/asm/asm

•開發者指南:https://asm.ow2.io/developer-guide.html

2、ASM能做什麼

生成、修改、刪除(介面、類、欄位、方法...)ASM能夠對位元組碼資料進行analyze、generate、transformation,ASM可以形象的理解為“Java語言世界”邊緣上一扇大門,透過這扇大門,可以幫助我們進入到“位元組碼的世界”。

3、ASM實際的使用場景

3.1、Spring當中的ASM

第一個應用場景,是Spring框架當中的AOP。 在很多Java專案中,都會使用到Spring框架,而Spring框架當中的AOP(Aspect Oriented Programming)是依賴於ASM的。具體來說,Spring的AOP,可以透過JDK的動態代理來實現,也可以透過CGLIB實現。其中,CGLib (Code Generation Library)是在ASM的基礎上構建起來的,所以,Spring AOP是間接的使用了ASM。(參考自 Spring Framework Reference Documentation的 8.6 Proxying mechanisms)。

3.2、JDK當中的ASM

第二個應用場景,是JDK當中的Lambda表示式。 在Java 8中引入了一個非常重要的特性,就是支援Lambda表示式。Lambda表示式,允許把方法作為引數進行傳遞,它能夠使程式碼變的更加簡潔緊湊。但是,我們可能沒有注意到,其實,在現階段(Java 8版本),Lambda表示式的呼叫是透過ASM來實現的。

在rt.jar檔案的jdk.internal.org.objectweb.asm包當中,就包含了JDK內建的ASM程式碼。在JDK 8版本當中,它所使用的ASM 5.0版本。

如果我們跟蹤Lambda表示式的編碼實現,就會找到InnerClassLambdaMetafactory.spinInnerClass()方法。在這個方法當中,我們就會看到:JDK會使用jdk.internal.org.objectweb.asm.ClassWriter來生成一個類,將lambda表示式的程式碼包裝起來。

LambdaMetafactory.metafactory() 第一步,找到這個方法​ InnerClassLambdaMetafactory.buildCallSite() 第二步,找到這個方法

​ InnerClassLambdaMetafactory.spinInnerClass() 第三步,找到這個方法

4、 ASM的兩個組成部分

從組成結構上來說,ASM分成兩部分,一部分為Core API,另一部分為Tree API。

其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;其中,Tree API包括asm-tree.jar和asm-analysis.jar。

asm.jar核心心類:ClassReader、ClassVisitor、ClassWriter、FieldVisitor、FieldWriter、MethodVisitor、MethodWriter、Label、Opcodes、Type

ClassReader類,負責讀取.class檔案裡的內容,然後拆分成各個不同的部分。ClassVisitor類,負責對.class檔案中某一部分裡的資訊進行修改。ClassWriter類,負責將各個不同的部分重新組合成一個完整的.class檔案。

asm-util.jar核心心類

以Check開頭的類,主要負責檢查(Check)生成的.class檔案內容是否正確。以Trace開頭的類,主要負責將.class檔案的內容列印成文字輸出。根據輸出的文字資訊,可以探索或追蹤(Trace).class檔案的內部資訊。

5、ClassFile

我們都知道,在.class檔案中,儲存的是ByteCode資料。但是,這些ByteCode資料並不是雜亂無章的,而是遵循一定的資料結構。

這個.class檔案遵循的資料結構就是由 Java Virtual Machine Specification中定義的 The class File Format

6、常見的位元組碼類庫

Apache Commons BCEL:其中BCEL為Byte Code Engineering Library首字母的縮寫。

Javassist:Javassist表示Java programming assistant

ObjectWeb ASM:本課程的研究物件。

Byte Buddy:在ASM基礎上實現的一個類庫。

二、無中生有

1、生成新的介面

預期目標:

生成一個正常介面結構定義的.class檔案

public interface ASMInterface {
    byte byteType = 1;
    short shortType = 1;
    int intType = 1;
    char charType = 's';
    float floatType = 1.1F;
    double doubleType = 1.2;
    long longType = 1L;
    boolean booleanType = false;
    Byte ByteType = 1;
    Short ShortType = Short.valueOf((short)1);
    Integer IntegerType = 1;
    String StringType = "s";
    Float FloatType = 1.1F;
    Double DoubleType = 1.1;
    Long LongType = 1L;
    Boolean BooleanType = true;

    void function();

    default String defaultFunction(Integer integer) {
        System.out.println("param = " + integer);
        return String.valueOf(integer);
    }

    static Integer getInteger(String str) {
        return Integer.valueOf(str);
    }
}

編碼實現:

public class InterfaceGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/ASMGenerateInterface.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]內容
        byte[] bytes = dump();

        // (2) 儲存byte[]到檔案
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 建立ClassWriter物件
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 呼叫visitXxx()方法,呼叫順序和說明如下
        /*
         *  visit
         *  [visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
         *  (visitAnnotation |
         *   visitTypeAnnotation |
         *   visitAttribute)*
         *  (visitNestMember |
         *   visitInnerClass |
         *   visitRecordComponent |
         *   visitField |
         *   visitMethod)*
         *  visitEnd
         *  []: 表示最多呼叫一次,可以不呼叫,但最多呼叫一次。
         *  ()和|: 表示在多個方法之間,可以選擇任意一個,並且多個方法之間不分前後順序。
         *  *: 表示方法可以呼叫0次或多次。
         * */

        //定義介面
        /*
         *visit(version, access, name, signature, superName, interfaces)
         *version: 表示當前類的版本資訊。在下述示例程式碼中,其取值為Opcodes.V1_8,表示使用Java 8版本。
         *access: 表示當前類的訪問標識(access flag)資訊。在下面的示例中,access的取值是ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,也可以寫成ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE。如果想進一步瞭解這些標識的含義,可以參考Java Virtual Machine Specification的Chapter 4. The class File Format部分。
         *name: 表示當前類的名字,它採用的格式是Internal Name的形式。在.java檔案中,我們使用Java語言來編寫程式碼,使用類名的形式是Fully Qualified Class Name,例如java.lang.String;將.java檔案編譯之後,就會生成.class檔案;在.class檔案中,類名的形式會發生變化,稱之為Internal Name,例如java/lang/String。因此,將Fully Qualified Class Name轉換成Internal Name的方式就是,將.字元轉換成/字元。
         *signature: 表示當前類的泛型資訊。因為在這個介面當中不包含任何的泛型資訊,因此它的值為null。
         *superName: 表示當前類的父類資訊,它採用的格式是Internal Name的形式。
         *interfaces: 表示當前類實現了哪些介面資訊。
         **/
        cw.visit(
                V1_8,                                        // version
                ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,   // access
                "sample/ASMGenerateInterface",               // name
                null,                                        // signature
                "java/lang/Object",                          // superName
                null                                         // interfaces
        );
        //定義欄位-基本型別
        /*
         * visitField(access, name, descriptor, signature, value)
         *access引數:表示當前欄位或方法帶有的訪問標識(access flag)資訊,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
         *name引數:表示當前欄位或方法的名字。
         *descriptor引數:表示當前欄位或方法的描述符。這些描述符,與我們平時使用的Java型別是有區別的。byte-B、short-S、int-I、char-C、具體可以參考如下示例程式碼
         *signature引數:表示當前欄位或方法是否帶有泛型資訊。換句話說,如果不帶有泛型資訊,提供一個null就可以了;如果帶有泛型資訊,就需要給它提供某一個具體的值。
         *value引數:是visitField()方法的第5個引數。這個引數的取值,與當前欄位是否為常量有關係。如果當前欄位是一個常量,就需要給value引數提供某一個具體的值;如果當前欄位不是常量,那麼使用null就可以了。
         * */
        {
            FieldVisitor fv1 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "byteType", "B", null, new Integer(1));
            fv1.visitEnd();
        }
        {
            FieldVisitor fv2 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "shortType", "S", null, new Integer(1));
            fv2.visitEnd();
        }
        {
            FieldVisitor fv3 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "intType", "I", null, new Integer(1));
            fv3.visitEnd();
        }
        {
            FieldVisitor fv4 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "charType", "C", null, 's');
            fv4.visitEnd();
        }
        {
            FieldVisitor fv5 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "floatType", "F", null, new Float("1.1"));
            fv5.visitEnd();
        }
        {
            FieldVisitor fv6 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "doubleType", "D", null, new Double("1.2"));
            fv6.visitEnd();
        }
        {
            FieldVisitor fv7 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "longType", "J", null, new Long(1L));
            fv7.visitEnd();
        }
        {
            FieldVisitor fv8 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "booleanType", "Z", null, new Integer(0));
            fv8.visitEnd();
        }
        //定義變數-包裝型別
        {
            FieldVisitor fv11 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "ByteType", "Ljava/lang/Byte;", null, null);
            fv11.visitEnd();
        }
        {
            FieldVisitor fv12 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "ShortType", "Ljava/lang/Short;", null,null);
            fv12.visitEnd();
        }
        {
            FieldVisitor fv13 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "IntegerType", "Ljava/lang/Integer;", null,null);
            fv13.visitEnd();
        }
        {
            FieldVisitor fv14 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "StringType", "Ljava/lang/String;", null, "s");
            fv14.visitEnd();
        }
        {
            FieldVisitor fv15 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "FloatType", "Ljava/lang/Float;", null,null);
            fv15.visitEnd();
        }
        {
            FieldVisitor fv16 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "DoubleType", "Ljava/lang/Double;", null,null);
            fv16.visitEnd();
        }
        {
            FieldVisitor fv17 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "LongType", "Ljava/lang/Long;", null,  null);
            fv17.visitEnd();
        }
        {
            FieldVisitor fv18 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "BooleanType", "Ljava/lang/Boolean;", null, null);
            fv18.visitEnd();
        }
        //定義方法-抽象方法
        /*
         * visitMethod(access, name, descriptor, signature, exceptions)
         *access引數:表示當前欄位或方法帶有的訪問標識(access flag)資訊,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
         *name引數:表示當前欄位或方法的名字。
         *descriptor引數:表示當前欄位或方法的描述符。這些描述符,與我們平時使用的Java型別是有區別的。()內為入參,後面為反參
         *signature引數:表示當前欄位或方法是否帶有泛型資訊。換句話說,如果不帶有泛型資訊,提供一個null就可以了;如果帶有泛型資訊,就需要給它提供某一個具體的值。
         *exceptions引數:是visitMethod()方法的第5個引數。這個引數的取值,與當前方法宣告中是否具有throws XxxException相關。
         * */
        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "function", "()V", null, null);
            mv1.visitEnd();
        }
        //定義方法-預設方法
        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "defaultFunction", "(Ljava/lang/Integer;)Ljava/lang/String;", null, null);
            mv2.visitCode();
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv2.visitInsn(DUP);
            mv2.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv2.visitLdcInsn("param = ");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv2.visitVarInsn(ALOAD, 1);
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitVarInsn(ALOAD, 1);
            mv2.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false);
            mv2.visitInsn(ARETURN);
            mv2.visitMaxs(3, 2);
            mv2.visitEnd();
        }
        //定義方法-靜態方法
        {
            MethodVisitor mv3 = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "getInteger", "(Ljava/lang/String;)Ljava/lang/Integer;", null, null);
            mv3.visitCode();
            mv3.visitVarInsn(ALOAD, 0);
            mv3.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(Ljava/lang/String;)Ljava/lang/Integer;", false);
            mv3.visitInsn(ARETURN);
            mv3.visitMaxs(1, 1);
            mv3.visitEnd();
        }

        {
            MethodVisitor mv4 = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
            mv4.visitCode();
            mv4.visitInsn(ICONST_1);
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "ByteType", "Ljava/lang/Byte;");
            mv4.visitInsn(ICONST_1);
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "ShortType", "Ljava/lang/Short;");
            mv4.visitInsn(ICONST_1);
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "IntegerType", "Ljava/lang/Integer;");
            mv4.visitLdcInsn(new Float("1.1"));
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "FloatType", "Ljava/lang/Float;");
            mv4.visitLdcInsn(new Double("1.1"));
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "DoubleType", "Ljava/lang/Double;");
            mv4.visitInsn(LCONST_1);
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "LongType", "Ljava/lang/Long;");
            mv4.visitInsn(ICONST_1);
            mv4.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
            mv4.visitFieldInsn(PUTSTATIC, "sample/ASMInterface", "BooleanType", "Ljava/lang/Boolean;");
            mv4.visitInsn(RETURN);
            mv4.visitMaxs(2, 0);
            mv4.visitEnd();
        }
        cw.visitEnd(); // 注意,最後要呼叫visitEnd()方法
        // (3) 呼叫toByteArray()方法
        return cw.toByteArray();
    }
}

驗證結果:

生成的介面是否正確

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class<?> clazz = classLoader.loadClass("sample.ASMGenerateInterface");
        Field[] declaredFields = clazz.getDeclaredFields();
        if (declaredFields.length > 0) {
            System.out.println("fields:");
            for (Field f : declaredFields) {
                Object value = f.get(null);
                System.out.println("    " + f.getName() + ": " + value);
            }
        }
        Method[] declaredMethods = clazz.getDeclaredMethods();
        if (declaredMethods.length > 0) {
            System.out.println("methods:");
            for (Method m : declaredMethods) {
                System.out.println("    " + m.getName());
            }
        }
    }
}

效果圖如下:

2、生成新的類

預期目標:

生成一個正常類結構定義的.class檔案

public class ASMClass {
    //定義變數-基本型別
    byte byteType = 1;
    short shortType = 1;
    int intType = 1;
    char charType = 's';
    float floatType = 1.1f;
    double doubleType = 1.2;
    long longType = 1;
    boolean booleanType = false;
    //定義變數-包裝型別
    Byte ByteType = 1;
    Short ShortType = 1;
    Integer IntegerType = 1;
    String StringType = "string";
    Float FloatType = 1.1f;
    Double DoubleType = 1.1;
    Long LongType = 1l;
    @Deprecated
    Boolean BooleanType = true;
    /*
     * 靜態方法
     * */
    public static Integer getInteger(String str) {
        return Integer.valueOf(str);
    }
    /*
     * 例項方法
     * */
    public String instanceMethod(Integer integer) {
        return String.valueOf(integer);
    }
}

編碼實現:

public class ClassGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/ASMGenerateClass.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]內容
        byte[] bytes = dump();

        // (2) 儲存byte[]到檔案
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 建立ClassWriter物件
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 呼叫visitXxx()方法,呼叫順序和說明如下
        /*
         *  visit
         *  [visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
         *  (visitAnnotation |
         *   visitTypeAnnotation |
         *   visitAttribute)*
         *  (visitNestMember |
         *   visitInnerClass |
         *   visitRecordComponent |
         *   visitField |
         *   visitMethod)*
         *  visitEnd
         *  []: 表示最多呼叫一次,可以不呼叫,但最多呼叫一次。
         *  ()和|: 表示在多個方法之間,可以選擇任意一個,並且多個方法之間不分前後順序。
         *  *: 表示方法可以呼叫0次或多次。
         * */

        //定義介面
        /*
         *visit(version, access, name, signature, superName, interfaces)
         *version: 表示當前類的版本資訊。在下述示例程式碼中,其取值為Opcodes.V1_8,表示使用Java 8版本。
         *access: 表示當前類的訪問標識(access flag)資訊。在下面的示例中,access的取值是ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,也可以寫成ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE。如果想進一步瞭解這些標識的含義,可以參考Java Virtual Machine Specification的Chapter 4. The class File Format部分。
         *name: 表示當前類的名字,它採用的格式是Internal Name的形式。在.java檔案中,我們使用Java語言來編寫程式碼,使用類名的形式是Fully Qualified Class Name,例如java.lang.String;將.java檔案編譯之後,就會生成.class檔案;在.class檔案中,類名的形式會發生變化,稱之為Internal Name,例如java/lang/String。因此,將Fully Qualified Class Name轉換成Internal Name的方式就是,將.字元轉換成/字元。
         *signature: 表示當前類的泛型資訊。因為在這個介面當中不包含任何的泛型資訊,因此它的值為null。
         *superName: 表示當前類的父類資訊,它採用的格式是Internal Name的形式。
         *interfaces: 表示當前類實現了哪些介面資訊。
         **/
        cw.visit(
                V1_8,                                        // version
                ACC_PUBLIC + ACC_SUPER,   // access
                "sample/ASMGenerateClass",               // name
                null,                                        // signature
                "java/lang/Object",                          // superName
                null                                         // interfaces
        );
        //定義欄位-基本型別
        /*
         * visitField(access, name, descriptor, signature, value)
         *access引數:表示當前欄位或方法帶有的訪問標識(access flag)資訊,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
         *name引數:表示當前欄位或方法的名字。
         *descriptor引數:表示當前欄位或方法的描述符。這些描述符,與我們平時使用的Java型別是有區別的。byte-B、short-S、int-I、char-C、具體可以參考如下示例程式碼
         *signature引數:表示當前欄位或方法是否帶有泛型資訊。換句話說,如果不帶有泛型資訊,提供一個null就可以了;如果帶有泛型資訊,就需要給它提供某一個具體的值。
         *value引數:是visitField()方法的第5個引數。這個引數的取值,與當前欄位是否為常量有關係。如果當前欄位是一個常量,就需要給value引數提供某一個具體的值;如果當前欄位不是常量,那麼使用null就可以了。
         * */
        {
            FieldVisitor fv1 = cw.visitField(0, "byteType", "B", null, new Byte("1"));
            fv1.visitEnd();
        }
        {
            FieldVisitor fv2 = cw.visitField(0, "shortType", "S", null, new Short("1"));
            fv2.visitEnd();
        }
        {
            FieldVisitor fv3 = cw.visitField(0, "intType", "I", null, new Integer(1));
            fv3.visitEnd();
        }
        {
            FieldVisitor fv4 = cw.visitField(0, "charType", "C", null, "s");
            fv4.visitEnd();
        }
        {
            FieldVisitor fv5 = cw.visitField(0, "floatType", "F", null, new Float("1.1"));
            fv5.visitEnd();
        }
        {
            FieldVisitor fv6 = cw.visitField(0, "doubleType", "D", null, new Double("1.2"));
            fv6.visitEnd();
        }
        {
            FieldVisitor fv7 = cw.visitField(0, "longType", "J", null, new Long(1));
            fv7.visitEnd();
        }
        {
            FieldVisitor fv8 = cw.visitField(0, "booleanType", "Z", null, false);
            fv8.visitEnd();
        }
        //定義變數-包裝型別
        {
            FieldVisitor fv11 = cw.visitField(0, "ByteType", "Ljava/lang/Byte;", null, 1);
            fv11.visitEnd();
        }
        {
            FieldVisitor fv12 = cw.visitField(0, "ShortType", "Ljava/lang/Short;", null, 1);
            fv12.visitEnd();
        }
        {
            FieldVisitor fv13 = cw.visitField(0, "IntegerType", "Ljava/lang/Integer;", null, 1);
            fv13.visitEnd();
        }
        {
            FieldVisitor fv14 = cw.visitField(0, "StringType", "Ljava/lang/String;", null, "s");
            fv14.visitEnd();
        }
        {
            FieldVisitor fv15 = cw.visitField(0, "FloatType", "Ljava/lang/Float;", null, 1.1f);
            fv15.visitEnd();
        }
        {
            FieldVisitor fv16 = cw.visitField(ACC_PUBLIC, "DoubleType", "Ljava/lang/Double;", null, 1.1);
            fv16.visitEnd();
        }
        {
            FieldVisitor fv17 = cw.visitField(0, "LongType", "Ljava/lang/Long;", null, 1l);
            fv17.visitEnd();
        }
        {
            FieldVisitor fv18 = cw.visitField(ACC_DEPRECATED, "BooleanType", "Ljava/lang/Boolean;", null, true);
            {
                AnnotationVisitor annotationVisitor0 = fv18.visitAnnotation("Ljava/lang/Deprecated;", true);
                annotationVisitor0.visitEnd();
            }
            fv18.visitEnd();
        }
        /*
         * visitMethod(access, name, descriptor, signature, exceptions)
         *access引數:表示當前欄位或方法帶有的訪問標識(access flag)資訊,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
         *name引數:表示當前欄位或方法的名字。
         *descriptor引數:表示當前欄位或方法的描述符。這些描述符,與我們平時使用的Java型別是有區別的。()內為入參,後面為反參
         *signature引數:表示當前欄位或方法是否帶有泛型資訊。換句話說,如果不帶有泛型資訊,提供一個null就可以了;如果帶有泛型資訊,就需要給它提供某一個具體的值。
         *exceptions引數:是visitMethod()方法的第5個引數。這個引數的取值,與當前方法宣告中是否具有throws XxxException相關。
         * */
        //定義方法-靜態程式碼塊
        {
            MethodVisitor mv2 = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
            mv2.visitCode();
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("class initialization method");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(2, 0);
            mv2.visitEnd();
        }
        //定義方法-無參構造器
        {
            MethodVisitor methodVisitor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "byteType", "B");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "shortType", "S");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "intType", "I");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitIntInsn(BIPUSH, 115);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "charType", "C");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitLdcInsn(new Float("1.1"));
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "floatType", "F");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitLdcInsn(new Double("1.2"));
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "doubleType", "D");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(LCONST_1);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "longType", "J");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_0);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "booleanType", "Z");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "ByteType", "Ljava/lang/Byte;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "ShortType", "Ljava/lang/Short;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "IntegerType", "Ljava/lang/Integer;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitLdcInsn("string");
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "StringType", "Ljava/lang/String;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitLdcInsn(new Float("1.1"));
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "FloatType", "Ljava/lang/Float;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitLdcInsn(new Double("1.1"));
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "DoubleType", "Ljava/lang/Double;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(LCONST_1);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "LongType", "Ljava/lang/Long;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitInsn(ICONST_1);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
            methodVisitor.visitFieldInsn(PUTFIELD, "sample/ASMGenerateClass", "BooleanType", "Ljava/lang/Boolean;");
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(3, 1);
            methodVisitor.visitEnd();

        }
        //定義方法-靜態方法
        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "getInteger", "(Ljava/lang/String;)Ljava/lang/Integer;", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(Ljava/lang/String;)Ljava/lang/Integer;", false);
            mv1.visitInsn(ARETURN);
            mv1.visitMaxs(1, 1);
            mv1.visitEnd();
        }
        //定義方法-例項方法
        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "instanceMethod", "(Ljava/lang/Integer;)Ljava/lang/String;", null, null);
            mv2.visitCode();
            mv2.visitVarInsn(ALOAD, 1);
            mv2.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false);
            mv2.visitInsn(ARETURN);
            mv2.visitMaxs(1, 2);
            mv2.visitEnd();
        }

        cw.visitEnd(); // 注意,最後要呼叫visitEnd()方法
        // (3) 呼叫toByteArray()方法
        return cw.toByteArray();
    }
}

驗證結果:

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.ASMGenerateClass");
        Method method = clazz.getDeclaredMethod("instanceMethod",Integer.class);
        Object instance = clazz.newInstance();
        Object invoke = method.invoke(instance, new Integer(12));
        Class<?> aClass = invoke.getClass();
        System.out.println("aClass = " + aClass);
    }
}
或者
public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class<?> clazz = classLoader.loadClass("sample.ASMGenerateClass");
        Field[] declaredFields = clazz.getDeclaredFields();
        if (declaredFields.length > 0) {
            for (Field f : declaredFields) {
                Object value = f.get(null);
                System.out.println("    " + f.getName() + ": " + value);
            }
        }
        Method[] declaredMethods = clazz.getDeclaredMethods();
        if (declaredMethods.length > 0) {
            for (Method m : declaredMethods) {
                System.out.println("    " + m.getName());
            }
        }
    }
}

效果圖如下:

ClassVisitor中visitXxx()的呼叫順序

visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
 visitAnnotation |
 visitTypeAnnotation |
 visitAttribute
)*
(
 visitNestMember |
 visitInnerClass |
 visitRecordComponent |
 visitField |
 visitMethod
)* 
visitEnd
其中,涉及到一些符號,它們的含義如下:
[]: 表示最多呼叫一次,可以不呼叫,但最多呼叫一次。
()和|: 表示在多個方法之間,可以選擇任意一個,並且多個方法之間不分前後順序。
*: 表示方法可以呼叫0次或多次。

FieldVisitor中visitXxx()的呼叫順序

(
 visitAnnotation |
 visitTypeAnnotation |
 visitAttribute
)*
visitEnd

MethodVisitor中visitXxx()的呼叫順序

(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
    visitCode
    (
        visitFrame |
        visitXxxInsn |
        visitLabel |
        visitInsnAnnotation |
        visitTryCatchBlock |
        visitTryCatchAnnotation |
        visitLocalVariable |
        visitLocalVariableAnnotation |
        visitLineNumber
    )*
    visitMaxs
]
visitEnd
第一組,在visitCode()方法之前的方法。這一組的方法,主要負責parameter、annotation和attributes等內容
第二組,在visitCode()方法和visitMaxs()方法之間的方法。這一組的方法,主要負責當前方法的“方法體”內的opcode內容。其中,visitCode()方法,標誌著方法體的開始,而visitMaxs()方法,標誌著方法體的結束。
第三組,是visitEnd()方法。這個visitEnd()方法,是最後一個進行呼叫的方法。

不同的MethodVisitor物件,它們的visitXxx()方法是彼此獨立的,只要各自遵循方法的呼叫順序,就能夠得到正確的結果。

三、狸貓換太子

1、修改類的版本

ClassVisitor子類實現

public class ClassChangeVersionVisitor extends ClassVisitor {
    public ClassChangeVersionVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(Opcodes.V1_7, access, name, signature, superName, interfaces);
    }
}

使用ClassVisitor的子類ClassChangeVersionVisitor進行類的版本修改

public class ASMModifyClass {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes = FileUtils.readBytes(filepath);

        //(1)構建ClassReader
        ClassReader cr = new ClassReader(bytes);

        //(2)構建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串連ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new ClassChangeVersionVisitor(api, cw);

        //(4)結合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

驗證&效果

透過javap -p -v HelloWorld命令可以看到版本號資訊已從52調整位51

2.給每個方法新增計算呼叫時間

對目標類進行方法改造調換---為每個方法新增用時計算


public class HelloWorld {
    public int add(int a, int b) throws InterruptedException {
        int c = a + b;
        Random rand = new Random(System.currentTimeMillis());
        int num = rand.nextInt(300);
        Thread.sleep(100 + num);
        return c;
    }

    public int sub(int a, int b) throws InterruptedException {
        int c = a - b;
        Random rand = new Random(System.currentTimeMillis());
        int num = rand.nextInt(400);
        Thread.sleep(100 + num);
        return c;
    }
}

ASM編碼實現


public class MethodTimerVisitor2 extends ClassVisitor {
    private String owner;
    private boolean isInterface;

    public MethodTimerVisitor2(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (!isInterface && mv != null && !"<init>".equals(name) && !"<clinit>".equals(name)) {
            boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
            boolean isNativeMethod = (access & ACC_NATIVE) != 0;
            if (!isAbstractMethod && !isNativeMethod) {
                // 每遇到一個合適的方法,就新增一個相應的欄位
                FieldVisitor fv = super.visitField(ACC_PUBLIC | ACC_STATIC, getFieldName(name), "J", null, null);
                if (fv != null) {
                    fv.visitEnd();
                }

                mv = new MethodTimerAdapter2(api, mv, owner, name);
            }

        }
        return mv;
    }


    private String getFieldName(String methodName) {
        return "timer_" + methodName;
    }

    private class MethodTimerAdapter2 extends MethodVisitor {
        private final String owner;
        private final String methodName;

        public MethodTimerAdapter2(int api, MethodVisitor mv, String owner, String methodName) {
            super(api, mv);
            this.owner = owner;
            this.methodName = methodName;
        }

        @Override
        public void visitCode() {
            // 首先,處理自己的程式碼邏輯
            super.visitFieldInsn(GETSTATIC, owner, getFieldName(methodName), "J"); // 注意,欄位名字要對應
            super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            super.visitInsn(LSUB);
            super.visitFieldInsn(PUTSTATIC, owner, getFieldName(methodName), "J"); // 注意,欄位名字要對應

            // 其次,呼叫父類的方法實現
            super.visitCode();
        }

        @Override
        public void visitInsn(int opcode) {
            // 首先,處理自己的程式碼邏輯
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                super.visitFieldInsn(GETSTATIC, owner, getFieldName(methodName), "J"); // 注意,欄位名字要對應
                super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                super.visitInsn(LADD);
                super.visitFieldInsn(PUTSTATIC, owner, getFieldName(methodName), "J"); // 注意,欄位名字要對應
            }

            // 其次,呼叫父類的方法實現
            super.visitInsn(opcode);
        }
    }
}

對方法進行轉換


public class HelloWorldTransformCore {
    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
  
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String dir = HelloWorldTransformCore.class.getResource("/").getPath();
        String filepath = dir + relative_path;
        File file = new File(filepath);
        try {
            InputStream in = new FileInputStream(file);
            in = new BufferedInputStream(in);
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            copyLarge(in, bao, new byte[DEFAULT_BUFFER_SIZE]);
            byte[] bytes1 = bao.toByteArray();

            //(1)構建ClassReader
            ClassReader cr = new ClassReader(bytes1);

            //(2)構建ClassWriter
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

            //(3)串連ClassVisitor
            int api = Opcodes.ASM9;
            ClassVisitor cv = new MethodTimerVisitor2(api, cw);

            //(4)結合ClassReader和ClassVisitor
            int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
            cr.accept(cv, parsingOptions);

            //(5)生成byte[]
            byte[] bytes2 = cw.toByteArray();
            OutputStream out = new FileOutputStream(filepath);
            BufferedOutputStream buff = new BufferedOutputStream(out);
            buff.write(bytes2);
            buff.flush();
            buff.close();
            System.out.println("file://" + filepath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer)
            throws IOException {
        long count = 0;
        int n;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }
}

驗證結果

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        // 第一部分,先讓“子彈飛一會兒”,讓程式執行一段時間
        HelloWorld instance = new HelloWorld();
        Random rand = new Random(System.currentTimeMillis());
        for (int i = 0; i < 10; i++) {
            boolean flag = rand.nextBoolean();
            int a = rand.nextInt(50);
            int b = rand.nextInt(50);
            if (flag) {
                int c = instance.add(a, b);
                String line = String.format("%d + %d = %d", a, b, c);
                System.out.println(line);
            }
            else {
                int c = instance.sub(a, b);
                String line = String.format("%d - %d = %d", a, b, c);
                System.out.println(line);
            }
        }

        // 第二部分,來檢視方法執行的時間
        Class<?> clazz = HelloWorld.class;
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field f : declaredFields) {
            String fieldName = f.getName();
            f.setAccessible(true);
            if (fieldName.startsWith("timer")) {
                Object FieldValue = f.get(null);
                System.out.println(fieldName + " = " + FieldValue);
            }
        }
    }
}

3、列印方法引數和返回值

對目標類進行方法改造---為每個方法新增列印入參和出參

public class HelloWorld {
    public int test(String name, int age, long idCard, Object obj) {
        int hashCode = 0;
        hashCode += name.hashCode();
        hashCode += age;
        hashCode += (int) (idCard % Integer.MAX_VALUE);
        hashCode += obj.hashCode();
        return hashCode;
    }
}

我們想實現的預期目標:列印出“方法接收的引數值”和“方法的返回值”。

public class HelloWorld {
    public int test(String name, int age, long idCard, Object obj) {

        int hashCode = 0;
        hashCode += name.hashCode();
        hashCode += age;
        hashCode += (int) (idCard % Integer.MAX_VALUE);
        hashCode += obj.hashCode();
        System.out.println(hashCode);
        return hashCode;
    }
}

實現這個功能的思路:在“方法進入”的時候,列印出“方法接收的引數值”;在“方法退出”的時候,列印出“方法的返回值”。

首先,我們新增一個ParameterUtils類,在這個類定義了許多print方法,這些print方法可以列印不同型別的資料。

public class ParameterUtils {
    private static final DateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void printValueOnStack(boolean value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(byte value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(char value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(short value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(int value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(float value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(long value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(double value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(Object value) {
        if (value == null) {
            System.out.println("    " + value);
        }
        else if (value instanceof String) {
            System.out.println("    " + value);
        }
        else if (value instanceof Date) {
            System.out.println("    " + fm.format(value));
        }
        else if (value instanceof char[]) {
            System.out.println("    " + Arrays.toString((char[])value));
        }
        else {
            System.out.println("    " + value.getClass() + ": " + value.toString());
        }
    }

    public static void printText(String str) {
        System.out.println(str);
    }
}

在下面的MethodParameterVisitor2類當中,我們將使用ParameterUtils類幫助我們列印資訊。

ASM編碼實現


public class MethodParameterVisitor2 extends ClassVisitor {
    public MethodParameterVisitor2(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (mv != null && !name.equals("<init>")) {
            boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
            boolean isNativeMethod = (access & ACC_NATIVE) != 0;
            if (!isAbstractMethod && !isNativeMethod) {
                mv = new MethodParameterAdapter2(api, mv, access, name, descriptor);
            }
        }
        return mv;
    }

    private static class MethodParameterAdapter2 extends MethodVisitor {
        private final int methodAccess;
        private final String methodName;
        private final String methodDesc;

        public MethodParameterAdapter2(int api, MethodVisitor mv, int methodAccess, String methodName, String methodDesc) {
            super(api, mv);
            this.methodAccess = methodAccess;
            this.methodName = methodName;
            this.methodDesc = methodDesc;
        }

        @Override
        public void visitCode() {
            // 首先,處理自己的程式碼邏輯
            boolean isStatic = ((methodAccess & ACC_STATIC) != 0);
            int slotIndex = isStatic ? 0 : 1;

            printMessage("Method Enter: " + methodName + methodDesc);

            Type methodType = Type.getMethodType(methodDesc);
            Type[] argumentTypes = methodType.getArgumentTypes();
            for (Type t : argumentTypes) {
                int sort = t.getSort();
                int size = t.getSize();
                String descriptor = t.getDescriptor();
                int opcode = t.getOpcode(ILOAD);
                super.visitVarInsn(opcode, slotIndex);
                if (sort >= Type.BOOLEAN && sort <= Type.DOUBLE) {
                    String methodDesc = String.format("(%s)V", descriptor);
                    printValueOnStack(methodDesc);
                }
                else {
                    printValueOnStack("(Ljava/lang/Object;)V");
                }

                slotIndex += size;
            }

            // 其次,呼叫父類的方法實現
            super.visitCode();
        }

        @Override
        public void visitInsn(int opcode) {
            // 首先,處理自己的程式碼邏輯
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                printMessage("Method Exit: " + methodName + methodDesc);
                if (opcode >= IRETURN && opcode <= DRETURN) {
                    Type methodType = Type.getMethodType(methodDesc);
                    Type returnType = methodType.getReturnType();
                    int size = returnType.getSize();
                    String descriptor = returnType.getDescriptor();

                    if (size == 1) {
                        super.visitInsn(DUP);
                    }
                    else {
                        super.visitInsn(DUP2);
                    }
                    String methodDesc = String.format("(%s)V", descriptor);
                    printValueOnStack(methodDesc);
                }
                else if (opcode == ARETURN) {
                    super.visitInsn(DUP);
                    printValueOnStack("(Ljava/lang/Object;)V");
                }
                else if (opcode == RETURN) {
                    printMessage("    return void");
                }
                else {
                    printMessage("    abnormal return");
                }
            }

            // 其次,呼叫父類的方法實現
            super.visitInsn(opcode);
        }

        private void printMessage(String str) {
            super.visitLdcInsn(str);
            super.visitMethodInsn(INVOKESTATIC, "sample/ParameterUtils", "printText", "(Ljava/lang/String;)V", false);
        }

        private void printValueOnStack(String descriptor) {
            super.visitMethodInsn(INVOKESTATIC, "sample/ParameterUtils", "printValueOnStack", descriptor, false);
        }
    }
}

進行轉換


public class HelloWorldTransformCore {
    public static void main(String[] args) {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);
        byte[] bytes1 = FileUtils.readBytes(filepath);

        //(1)構建ClassReader
        ClassReader cr = new ClassReader(bytes1);

        //(2)構建ClassWriter
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        //(3)串連ClassVisitor
        int api = Opcodes.ASM9;
        ClassVisitor cv = new MethodParameterVisitor2(api, cw);

        //(4)結合ClassReader和ClassVisitor
        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
        cr.accept(cv, parsingOptions);

        //(5)生成byte[]
        byte[] bytes2 = cw.toByteArray();

        FileUtils.writeBytes(filepath, bytes2);
    }
}

驗證結果

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        HelloWorld instance = new HelloWorld();
        int hashCode = instance.test("Tomcat", 10, System.currentTimeMillis(), new Object());
        int remainder = hashCode % 2;

        if (remainder == 0) {
            System.out.println("hashCode is even number.");
        }
        else {
            System.out.println("hashCode is odd number.");
        }
    }
}

四、非順序結構

1、if語句

public class HelloWorld {
    public void test(int value) {
        if (value == 0) {
            System.out.println("value is 0");
        }
        else {
            System.out.println("value is not 0");
        }
    }
}

ASM編碼實現


public class HelloWorldGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]內容
        byte[] bytes = dump();

        // (2) 儲存byte[]到檔案
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 建立ClassWriter物件
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 呼叫visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
            Label elseLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            mv2.visitCode();
            mv2.visitVarInsn(ILOAD, 1);
            mv2.visitJumpInsn(IFNE, elseLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("value is 0");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第2段
            mv2.visitLabel(elseLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("value is not 0");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 第3段
            mv2.visitLabel(returnLabel);
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(0, 0);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 呼叫toByteArray()方法
        return cw.toByteArray();
    }
}

驗證結果


public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Object obj = clazz.newInstance();

        Method method = clazz.getDeclaredMethod("test", int.class);
        method.invoke(obj, 0);
        method.invoke(obj, 1);
    }
}

2、switch語句

public class HelloWorld {
    public void test(int val) {
        switch (val) {
            case 1:
                System.out.println("val = 1");
                break;
            case 2:
                System.out.println("val = 2");
                break;
            case 3:
                System.out.println("val = 3");
                break;
            case 4:
                System.out.println("val = 4");
                break;
            default:
                System.out.println("val is unknown");
        }
    }
}

ASM編碼實現


public class HelloWorldGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]內容
        byte[] bytes = dump();

        // (2) 儲存byte[]到檔案
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 建立ClassWriter物件
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 呼叫visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
            Label caseLabel1 = new Label();
            Label caseLabel2 = new Label();
            Label caseLabel3 = new Label();
            Label caseLabel4 = new Label();
            Label defaultLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            mv2.visitCode();
            mv2.visitVarInsn(ILOAD, 1);
            mv2.visitTableSwitchInsn(1, 4, defaultLabel, new Label[]{caseLabel1, caseLabel2, caseLabel3, caseLabel4});

            // 第2段
            mv2.visitLabel(caseLabel1);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 1");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第3段
            mv2.visitLabel(caseLabel2);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 2");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第4段
            mv2.visitLabel(caseLabel3);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 3");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第5段
            mv2.visitLabel(caseLabel4);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 4");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第6段
            mv2.visitLabel(defaultLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val is unknown");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 第7段
            mv2.visitLabel(returnLabel);
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(0, 0);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 呼叫toByteArray()方法
        return cw.toByteArray();
    }
}

驗證結果

import java.lang.reflect.Method;

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Object obj = clazz.newInstance();

        Method method = clazz.getDeclaredMethod("test", int.class);
        for (int i = 1; i < 6; i++) {
            method.invoke(obj, i);
        }
    }
}

3、for語句

public class HelloWorld {
    public void test() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

ASM編碼實現

import org.objectweb.asm.*;

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

public class HelloWorldGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]內容
        byte[] bytes = dump();

        // (2) 儲存byte[]到檔案
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 建立ClassWriter物件
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 呼叫visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor methodVisitor = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
            Label conditionLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            methodVisitor.visitCode();
            methodVisitor.visitInsn(ICONST_0);
            methodVisitor.visitVarInsn(ISTORE, 1);

            // 第2段
            methodVisitor.visitLabel(conditionLabel);
            methodVisitor.visitVarInsn(ILOAD, 1);
            methodVisitor.visitIntInsn(BIPUSH, 10);
            methodVisitor.visitJumpInsn(IF_ICMPGE, returnLabel);
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitVarInsn(ILOAD, 1);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
            methodVisitor.visitIincInsn(1, 1);
            methodVisitor.visitJumpInsn(GOTO, conditionLabel);

            // 第3段
            methodVisitor.visitLabel(returnLabel);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(0, 0);
            methodVisitor.visitEnd();
        }

        cw.visitEnd();

        // (3) 呼叫toByteArray()方法
        return cw.toByteArray();
    }
}

驗證結果

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Object obj = clazz.newInstance();

        Method method = clazz.getDeclaredMethod("test");
        method.invoke(obj);
    }
}

4、try-catch語句

public class HelloWorld {
    public void test() {
        try {
            System.out.println("Before Sleep");
            Thread.sleep(1000);
            System.out.println("After Sleep");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ASM編碼實現

public class HelloWorldGenerateCore {
    public static void main(String[] args) throws Exception {
        String relative_path = "sample/HelloWorld.class";
        String filepath = FileUtils.getFilePath(relative_path);

        // (1) 生成byte[]內容
        byte[] bytes = dump();

        // (2) 儲存byte[]到檔案
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 建立ClassWriter物件
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 呼叫visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
            Label startLabel = new Label();
            Label endLabel = new Label();
            Label exceptionHandlerLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            mv2.visitCode();
            // visitTryCatchBlock可以在這裡訪問
            mv2.visitTryCatchBlock(startLabel, endLabel, exceptionHandlerLabel, "java/lang/InterruptedException");

            // 第2段
            mv2.visitLabel(startLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("Before Sleep");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitLdcInsn(new Long(1000L));
            mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("After Sleep");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 第3段
            mv2.visitLabel(endLabel);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第4段
            mv2.visitLabel(exceptionHandlerLabel);
            mv2.visitVarInsn(ASTORE, 1);
            mv2.visitVarInsn(ALOAD, 1);
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);

            // 第5段
            mv2.visitLabel(returnLabel);
            mv2.visitInsn(RETURN);

            // 第6段
            // visitTryCatchBlock也可以在這裡訪問
            // mv2.visitTryCatchBlock(startLabel, endLabel, exceptionHandlerLabel, "java/lang/InterruptedException");
            mv2.visitMaxs(0, 0);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 呼叫toByteArray()方法
        return cw.toByteArray();
    }
}

驗證結果

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("sample.HelloWorld");
        Object obj = clazz.newInstance();

        Method method = clazz.getDeclaredMethod("test");
        method.invoke(obj);
    }
}

五、檢視class檔案的ASM程式碼

1.列印

當我們想對某一類進行ASM學習或者對想要實現的功能不知道如何實現時可以使用如下類進行ASM程式碼輸出並檢視

public class ASMPrint {
    public static void main(String[] args) throws IOException {
        // (1) 設定引數
        String className = "sample.HelloWorld";
        int parsingOptions = ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG;
        boolean asmCode = true;

        // (2) 列印結果
        Printer printer = asmCode ? new ASMifier() : new Textifier();
        PrintWriter printWriter = new PrintWriter(System.out, true);
        TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, printer, printWriter);
        new ClassReader(className).accept(traceClassVisitor, parsingOptions);
    }
}

className值設定為類的全限定名,可以是我們自己寫的類,例如sample.HelloWorld,也可以是JDK自帶的類,例如java.lang.Comparable。

asmCode值設定為true或false。如果是true,可以列印出對應的ASM程式碼;如果是false,可以列印出方法對應的Instruction。

parsingOptions值設定為ClassReader.SKIP\_CODE、ClassReader.SKIP\_DEBUG、ClassReader.SKIP\_FRAMES、ClassReader.EXPAND\_FRAMES的組合值,也可以設定為0,可以列印出詳細程度不同的資訊。

2.外掛

如果你是IDEA可以安裝ASM ByteCode Viewer,然後選擇要檢視得java檔案右鍵選擇後可以在右側看到ASPPlugin上看到對應得ByteCode、ASMified、Groovified。或者是外掛ASM Bytecode Outline(本人用得社群版IDEA未達到效果)

六、TreeApi

Core API和Tree API的區別

•Tree API的優勢:易用性:如果一個人在之前並沒有接觸過Core API和Tree API,那麼Tree API更容易入手。功能性:在實現比較複雜的功能時,Tree API比Core API更容易實現。

•Core API的優勢:執行效率:在實現相同功能的前提下,Core API要比Tree API執行效率高,花費時間少。記憶體使用:Core API比Tree API佔用的記憶體空間少。

•第一點,在ASM當中,不管是Core API,還是Tree API,都能夠進行Class Generation、Class Transformation和Class Analysis操作。

•第二點,Core API和Tree API是兩者有各自的優勢。Tree API易於使用、更容易實現複雜的操作;Core API執行速度更快、佔用記憶體空間更少。

這裡由於篇幅限制做了刪減,如果有興趣可以聯絡作者進行交流

七、文中用到的工具類

1、FileUtils

public class FileUtils {
    public static String getFilePath(String relativePath) {
        String dir = FileUtils.class.getResource("/").getPath();
        return dir + relativePath;
    }
}

八、思考對於ASM我們以後能用於做些什麼?

1、生成類----根據模版生成類(結合腳手架快速搭建專案以及專案初期模組快速搭建)

2、修改類----根據模版修改類(結合特定結構在java原始碼編譯生成.class位元組碼檔案時對類進行修改以達到提升系統效率和釋放開發人力的目的,可以結合IDEA外掛開發開發類似於lombok或更為強大的外掛)

IDEA外掛開發可以參考:Intellij IDEA 外掛開發

腳手架開發:從0到1搭建自己的腳手架(java後端)

流程編排:流程編排及視覺化

作者:京東健康 馬仁喜

來源:京東雲開發者社群 轉載請註明來源