Common usage of Java ASM framework bytecode instrumentation

foreword

  ASM is a tool for reading and writing Java bytecodes. It can skip source code writing and compilation, create classes directly in the form of bytecodes, and modify the attributes and methods of existing classes (or classes in jars). It is usually used to develop some auxiliary frameworks for Java development. The method is to inject some specific code (commonly known as bytecode instrumentation) into the Java code you write to achieve a specific purpose. Taking Android development as an example, the most common method is to use bytecode. Instrumentation realizes unconventional operations such as hot repair, event monitoring, buried point, open source framework, etc. Of course, it is usually used with Gradle plugins in Android development, and this will be written another day.

background

I have heard of ASM and bytecode instrumentation technology for a long time, but I rarely use it directly in my work. Because I have this learning need recently, I specially made this note for students who need it, and also as my own reference note. for later use.

According to my practice, I found that there are at least the following three places where ASM API can be obtained

1. ASM official website: asm.ow2.io/ Here are all versions from 4.0 to the latest 9.3, you can download the corresponding jar package, as well as the manual asm.ow2.io/asm4-guide... , and then depend on It can be used in Java projects, and its API is not much different.

2. The asm api that comes with jdk (mine is JDK11)

3. The api that comes with gradle (because I am an Android developer, I used this method: when you use it, you only need to add dependencies


dependencies {
    implementation gradleApi()

//    testImplementation 'org.ow2.asm:asm:7.1'
//    testImplementation 'org.ow2.asm:asm-commons:7.1'
}
复制代码

In order to better refer to the bytecode, it is recommended to install ASM related plug-ins in Android Studio. As shown in the figure, you can install one of the three types. It is recommended to install the third type (you can only install one at most, otherwise your Android studio will not restart next time )

ASM_plug.png

Four common usage scenarios of ASM for reference by students in need

1. Generate a complete class (including several basic properties and methods)


package com.study;

import java.util.ArrayList;

public class Human {
    private String name;
    private long age;
    protected int no;
    public static long score;
    public static final String real_name = "Sand哥";

    public Human() {
    }

    public int greet(String var1) {
        System.out.println(var1);
        ArrayList var2 = new ArrayList();
        StringBuilder var3 = new StringBuilder();
        var3.append("Hello java asm StringBuilder");
        long var4 = System.nanoTime();
        return 10 + 11;
    }

    public static void staticMethod(String var0) {
        System.out.println("Hello Java Asm!");
    }
}
复制代码

How to generate with Java ASM (detailed comments where necessary)


public static void testCreateAClass()throws Exception{

        //新建一个类生成器,COMPUTE_FRAMES,COMPUTE_MAXS这2个参数能够让asm自动更新操作数栈
        ClassWriter cw=new ClassWriter(COMPUTE_FRAMES|COMPUTE_MAXS);
        //生成一个public的类,类路径是com.study.Human
        cw.visit(V1_8,ACC_PUBLIC,"com/study/Human",null,"java/lang/Object",null);

        //生成默认的构造方法: public Human()
        MethodVisitor mv=cw.visitMethod(ACC_PUBLIC,"<init>","()V",null,null);
        mv.visitVarInsn(ALOAD,0);
        mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>","()V",false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0,0);//更新操作数栈
        mv.visitEnd();//一定要有visitEnd

        //生成成员变量
        //1.生成String类型的成员变量:private String name;
        FieldVisitor fv= cw.visitField(ACC_PRIVATE,"name","Ljava/lang/String;",null,null);
        fv.visitEnd();//不要忘记end
        //2.生成Long类型成员:private long age
        fv=cw.visitField(ACC_PRIVATE,"age","J",null,null);
        fv.visitEnd();

        //3.生成Int类型成员:protected int no
        fv=cw.visitField(ACC_PROTECTED,"no","I",null,null);
        fv.visitEnd();

        //4.生成静态成员变量:public static long score
        fv=cw.visitField(ACC_PUBLIC+ACC_STATIC,"score","J",null,null);

        //5.生成常量:public static final String real_name = "Sand哥"
        fv=cw.visitField(ACC_PUBLIC+ACC_STATIC+ACC_FINAL,"real_name","Ljava/lang/String;",null,"Sand哥");
        fv.visitEnd();

        //6.生成成员方法greet
        mv=cw.visitMethod(ACC_PUBLIC,"greet","(Ljava/lang/String;)I",null,null);
        mv.visitCode();
        mv.visitIntInsn(ALOAD,0);
        mv.visitIntInsn(ALOAD,1);

        //6.1 调用静态方法 System.out.println("Hello");
        mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
//        mv.visitLdcInsn("Hello");//加载字符常量
        mv.visitIntInsn(ALOAD,1);//加载形参
        mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);//打印形参
        //6.2 创建局部变量
        LocalVariablesSorter lvs=new LocalVariablesSorter(ACC_PUBLIC,"(Ljava/lang/String;)I",mv);
        //创建ArrayList 对象
        //new ArrayList ,分配内存不初始化
        mv.visitTypeInsn(NEW,"java/util/ArrayList");
        mv.visitInsn(DUP);//压入栈
        //弹出一个对象所在的地址,进行初始化操作,构造函数默认为空,此时栈大小为1(到目前只有一个局部变量)
        mv.visitMethodInsn(INVOKESPECIAL,"java/util/ArrayList","<init>","()V",false);

        int time=lvs.newLocal(Type.getType(List.class));

        mv.visitVarInsn(ASTORE,time);
        mv.visitVarInsn(ALOAD,time);

        //创建StringBuilder对象
        mv.visitTypeInsn(NEW,"java/lang/StringBuilder");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);

        //这里需要注意在lvs.newLocal的时候使用Type.geteType("类路径") 会报错,需要改成Type.geteType("XXX.class“)的方式
        time=lvs.newLocal(Type.getType(StringBuilder.class));
        mv.visitVarInsn(ASTORE,time);
        mv.visitVarInsn(ALOAD,time);

        mv.visitLdcInsn("Hello java asm StringBuilder");
        mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/StringBuilder","append","(Ljava/lang/String;)Ljava/lang/StringBuilder;",false);

        mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","nanoTime","()J",false);
        time=lvs.newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE,time);
        mv.visitLdcInsn(10);
        mv.visitLdcInsn(11);
        mv.visitInsn(IADD);
        mv.visitInsn(IRETURN);
        mv.visitMaxs(0,0);
        mv.visitEnd();

        //生成静态方法
        mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "staticMethod", "(Ljava/lang/String;)V", null, null);
        //生成静态方法中的字节码指令
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Hello Java Asm!");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();

        //设置必要的类路径
        String path= PathUtils.getCurrentClassPath(DemoMain.class)+ File.separator+"Human.class";
        //获取类的byte数组
        byte[] classByteData=cw.toByteArray();
        //把类数据写入到class文件,这样你就可以把这个类文件打包供其他的人使用
        IOUtils.write(classByteData,new FileOutputStream(path));
        System.err.println("类生成的位置:"+path);
    }

复制代码
important point
1.ClassWriter(COMPUTE_FRAMES|COMPUTE_MAXS), the flag parameter of this ClassWriter, it is recommended to set the sum of these two. According to the official website, although the performance is poor, it can automatically update the operand stack and method call frame calculation.
2. Don't forget visitEnd at any time.
3.lvs.newLocal(Type.getType("com/xx/YYY")) when lvc creates a temporary variable; the method call may be wrong, change it to the following method int time=lvs.newLocal(Type.getType(YYY.class) ); can run normally, the specific reasons have not been studied.
4. Generate class summary

The class files generated by this method can be packaged and used by others, and Java object-oriented programming becomes bytecode-oriented programming. Of course, this usage also has a more readable Javapoet method, which will not be discussed here.

2. Modify existing classes (add properties, add methods, modify methods, etc.)

Suppose this class code is as follows (note the class file we modified), the asm code will be modified in 3 places

1. Added a phone field 2. Deleted the testA method 3. Changed the testC method to protected 4. Added a getPhone method

package com.test.javase_module;


public class TestFunction {

    private int a;
    public void testA(){
        System.out.println("I am A");
    }

    public void testB(){
        System.err.println("===>I am B");
    }

    public int testC(){
        return a;
    }
}
复制代码

The modified one is as follows (the modified class file can replace the original file and re-enter the jar package)

package com.test.javase_module;

public class TestFunction {
    private int a;
    //1.增加phone字段
    public String phone;

    public TestFunction() {
    }

    //2.已经删除了方法testA

    public void testB() {
        System.err.println("===>I am B");
    }

    //3.testC方法已经变成了protected
    protected int testC() {
        return this.a;
    }

    //4.增加了getPhone方法
    public String getPhone() {
        return this.phone;
    }
}
复制代码

The ASM code is as follows

 private static void testModifyCalss()throws Exception{
        ClassReader cr = new ClassReader("com.test.javase_module.TestFunction");
        final ClassWriter cw=new ClassWriter(cr,0);
//        cr.accept(cw, 0);//可以直接接受一个writer,实现复制
        cr.accept(new ClassVisitor(ASM4,cw) {//接受一个带classWriter的visitor,实现定制化方法拷贝或者属性删除字段
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                System.out.println("visit method:"+name+"====> "+descriptor);

                if("testA".equals(name)){//拷贝的过程中删除一个方法
                    return null;
                }

                if("testC".equals(name)){//将testC public方法变成protect
                    access=ACC_PROTECTED;
                }
                return super.visitMethod(access, name, descriptor, signature, exceptions);
            }

            @Override
            public void visitEnd() {
                //特别注意的是:要为类增加属性和方法,放到visitEnd中,避免破坏之前已经排列好的类结构,在结尾添加新结构
                //增加一个字段(注意不能重复),注意最后都要visitEnd
                FieldVisitor fv = cv.visitField(ACC_PUBLIC, "phone", "Ljava/lang/String;", null, null);
                fv.visitEnd();//不能缺少visitEnd

                //增加一个方法
                MethodVisitor mv=cv.visitMethod(ACC_PUBLIC,"getPhone","()Ljava/lang/String;",null,null);
                mv.visitCode();
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD,"com/test/javase_module/TestFunction","phone","Ljava/lang/String;");
                mv.visitInsn(IRETURN);
                mv.visitMaxs(1, 1);
                mv.visitEnd();//不能缺少visitEnd
                super.visitEnd();//注意原本的visiEnd不能少
            }
        },0);

        //指定新生成的class路径的生成位置,这个路径你可以随便指定
        String path=PathUtils.getCurrentClassPath(TestASM.class)+ File.separator+"TestFunction3.class";
        System.err.println("类生成的位置:"+path);
        IOUtils.write(cw.toByteArray(),new FileOutputStream(path));
    }
复制代码

3. Implement method injection (very versatile)

* 在一个方法的开始处和方法结束处增加自己的代码
* 当然还可以用ASM自带的AdviceAdapter来实现更简单
* 原来的方法如下
     public void testB() {
         System.err.println("===>I am B");
     }
*插装后反编译如下
     public void testB() {
         long var1 = System.currentTimeMillis();
         System.err.println("===>I am B");
         long var3 = System.currentTimeMillis();
         System.out.println((new StringBuilder()).append("cost:").append(var3 - var1).toString());
     }
复制代码

ASM code ( be careful to avoid constructors )

public static void testInspectCode() throws IOException {
        ClassReader cr = new ClassReader("com.test.javase_module.TestFunction");
        //--------------------------------------------------------------------------------
        //1.在使用 new ClassWriter(0)时,不会自动计算任何东西。必须自行计算帧、局部变 量与操作数栈的大小。
        //--------------------------------------------------------------------------------
        //2.在使用 new ClassWriter(ClassWriter.COMPUTE_MAXS)时,将为你计算局部变量与操作数栈部分的大小。
        // 还是必须调用 visitMaxs,但可以使用任何参数:它们将被忽略并重新计算。使用这一选项时,仍然必须自行计算这些帧。
        //--------------------------------------------------------------------------------
        //3.在 new ClassWriter(ClassWriter.COMPUTE_FRAMES)时,一切都是自动计算。不再需要调用 visitFrame,
        // 但仍然必须调用 visitMaxs(参数将被忽略并重新计算)
        //--------------------------------------------------------------------------------

        ClassWriter cw=new ClassWriter(cr,ClassWriter.COMPUTE_FRAMES+ClassWriter.COMPUTE_MAXS);
        cr.accept(new ClassVisitor(ASM4, cw) {

            @Override
            public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {

                MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);

                return new MethodVisitor(ASM4,mv) {

                    @Override
                    public void visitLineNumber(int line, Label start) {
                        System.out.println("经过这个测试行数:"+line+"可以对应到源代码的行数");
                        super.visitLineNumber(line, start);
                    }
                    @Override
                    public void visitParameter(String name, int access) {
                        super.visitParameter(name, access);
                    }

                    public void visitInsn(int opcode) {
                        //这里是访问语句结束,在return结束之前添加语句
                        //其中的 owner 必须被设定为所转换类的名字。现在必须在任意 RETURN 之前添加其他四条
                        //指令,还要在任何 xRETURN 或 ATHROW 之前添加,它们都是终止该方法执行过程的指令。这些
                        //指令没有任何参数,因此在 visitInsn 方法中访问。于是,可以重写这一方法,以增加指令:

                        if (!"<init>".equals(name) && (opcode >= Bytecodes.IRETURN && opcode <= Bytecodes.RETURN) || opcode == ATHROW) {
                            //在方法return之前添加代码
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System",  "currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,3);

                            mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream");
                            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                            mv.visitInsn(Bytecodes.DUP);

                            mv.visitMethodInsn(Bytecodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);
                            mv.visitLdcInsn("cost:");//就是传入一个字符串常量
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(Ljava/lang/String;)Ljava/lang/StringBuilder",false);
                            mv.visitVarInsn(LLOAD, 3);
                            mv.visitVarInsn(LLOAD,1);
                            mv.visitInsn(LSUB);
//
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
                        }
                        mv.visitInsn(opcode);
                    }

                    @Override
                    public void visitCode() {
                        super.visitCode();
                        //方法开始(可以在此处添加代码,在原来的方法之前执行)
                        System.out.println("方法名字=========>"+name);
                        if(!"<init>".equals(name)){//不要在构造方法中添加代码
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,1);
                        }
                    }
                };

            }
        },0);
        String path=PathUtils.getCurrentClassPath(DemoMain.class)+ File.separator+"TestFunction4.class";
        System.err.println("类生成的位置:"+path);
        IOUtils.write(cw.toByteArray(),new FileOutputStream(path));

    }
复制代码

4. Inject method calls

The Tool.useTool() method call is inserted at the end of the method. The specific content of this method can be written by yourself.


* 在原有的方法中插入字节码指令中插入方法调用
* 在testInspectCode的方法基础上添加com.utils.Tool#useTool(long) 的方法调用
* 原来的方法
    public void testA(){
         System.out.println("I am A");
     }
* 插入字节码后反编译如下
   public void testA() {
         long var1 = System.currentTimeMillis();
         System.out.println("I am A");
         long var3 = System.currentTimeMillis();
         System.out.println((new StringBuilder()).append("cost:").append(var3 - var1).toString());
         Tool.useTool(var1);//在这里调用了第三方的方法,大批量的注入改用方法调用注入,可以节省注入的字节码量
     }
复制代码

ASM code

public static void testInspectCode2()throws Exception{

        ClassReader cr = new ClassReader("com.test.javase_module.TestFunction");
        ClassWriter cw=new ClassWriter(cr,0);
        cr.accept(new ClassVisitor(ASM4, cw) {
            @Override
            public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {

                MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
                return new MethodVisitor(ASM4,mv) {

                    public void visitInsn(int opcode) {
               //这里是访问语句结束,在return结束之前添加语句
               //其中的 owner 必须被设定为所转换类的名字。现在必须在任意 RETURN 之前添加其他四条
               //指令,还要在任何 xRETURN 或 ATHROW 之前添加,它们都是终止该方法执行过程的指令。这些
                //指令没有任何参数,因此在 visitInsn 方法中访问。于是,可以重写这一方法,以增加指令:
                        if("<init>".equals(name)){
                            return;
                        }

                        if ((opcode >= Bytecodes.IRETURN && opcode <= Bytecodes.RETURN) || opcode == ATHROW) {
                            //在方法return之前添加代码

                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System",  "currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,3);

                            mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream");
                            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                            mv.visitInsn(Bytecodes.DUP);

                            mv.visitMethodInsn(Bytecodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);
                            mv.visitLdcInsn("cost:");//就是传入一个字符串常量
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(Ljava/lang/String;)Ljava/lang/StringBuilder",false);
                            mv.visitVarInsn(LLOAD, 3);
                            mv.visitVarInsn(LLOAD,1);
                            mv.visitInsn(LSUB);
//
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String",false);
                            mv.visitMethodInsn(Bytecodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);

                            mv.visitVarInsn(LLOAD, 1);
                            mv.visitMethodInsn(INVOKESTATIC,"com/utils/Tool","useTool","(J)V",false);

                        }
                        mv.visitInsn(opcode);
                    }

                    @Override
                    public void visitCode() {
                        super.visitCode();
                        //方法开始(可以在此处添加代码,在原来的方法之前执行)
                        if(!"<init>".equals(name)){
                            mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentTimeMillis", "()J",false);
                            mv.visitIntInsn(Bytecodes.LSTORE,1);
                        }
                    }

                };
            }
        },0);

        String path=PathUtils.getCurrentClassPath(DemoMain.class)+ File.separator+"TestFunction4.class";
        System.err.println("类生成的位置:"+path);
        IOUtils.write(cw.toByteArray(),new FileOutputStream(path));
    }
复制代码

5. Summary

The ASM function is actually very powerful, and there is a richer introduction in the official documentation

github:github.com/woshiwzy/as…

Guess you like

Origin juejin.im/post/7084911979281776647