• ASM字节码插桩:打印方法名、入参、返回值、方法耗时


    1. 预期目标

    • 使用ASM在进入方法后和退出方法前,对方法名、入参、返回值和执行耗时进行打印
    • 比如有一个Hello类
    package org.example.asm8.printArgs;
    
    import java.util.Date;
    
    public class Hello {
    
        private String name;
    
        private int age;
    
        private Date birthDate;
    
        public Hello(String name, int age, Date birthDate) {
            this.name = name;
            this.age = age;
            this.birthDate = birthDate;
        }
    
        public int hello() {
            if (age < 18) {
                throw new RuntimeException("too young");
            }
            return age;
        }
    
        public String testHello() {
            hello();
            return "success!";
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述

    • 进行字节码插桩后达到下面效果
    package org.example.asm8.printArgs;
    
    import java.util.Date;
    
    public class Hello {
        private String name;
        private int age;
        private Date birthDate;
    
        public Hello(String name, int age, Date birthDate) {
            long var6 = System.currentTimeMillis();
            PrintUtils.printText("Enter: 方法名 ,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V");
            PrintUtils.printObject("入参类型:", name);
            PrintUtils.printObject("入参类型:", new Integer(age));
            PrintUtils.printObject("入参类型:", birthDate);
            this.name = name;
            this.age = age;
            this.birthDate = birthDate;
            PrintUtils.printText("Exit: 方法名 ,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V");
            PrintUtils.printObject("返回值类型:", "void方法,没有返回值");
            PrintUtils.printSpendTime("", var6);
        }
    
        public int hello() {
            long var3 = System.currentTimeMillis();
            PrintUtils.printText("Enter: 方法名 hello,方法描述符 ()I");
            if (this.age < 18) {
                RuntimeException var5 = new RuntimeException("too young");
                PrintUtils.printText("Exit: 方法名 hello,方法描述符 ()I");
                PrintUtils.printObject("返回值类型:", "有异常抛出了");
                PrintUtils.printSpendTime("hello", var3);
                throw var5;
            } else {
                int var10000 = this.age;
                PrintUtils.printText("Exit: 方法名 hello,方法描述符 ()I");
                PrintUtils.printObject("返回值类型:", new Integer(var10000));
                PrintUtils.printSpendTime("hello", var3);
                return var10000;
            }
        }
    
        public String testHello() {
            long var3 = System.currentTimeMillis();
            PrintUtils.printText("Enter: 方法名 testHello,方法描述符 ()Ljava/lang/String;");
            this.hello();
            PrintUtils.printText("Exit: 方法名 testHello,方法描述符 ()Ljava/lang/String;");
            PrintUtils.printObject("返回值类型:", "success!");
            PrintUtils.printSpendTime("testHello", var3);
            return "success!";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    2. AdviceAdapter类

    • AdviceAdapter本身继承了MethodVisitor,提供了onMethodEnter()和onMethodExit()方法,分别表示方法进入和方法退出,方便添加我们自定义的处理逻辑。

    在这里插入图片描述

    • 通过注释可以看出,这两个方法都能够使用和修改局部变量表,但是最好不要改变操作数栈的状态
    • onMethodEnter在方法开始或者父类执行完构造方法后被调用
    • onMethodExit在方法结束且在return和athrow指令前被调用,操作数栈顶包含返回值和异常对象
    • onMethodEnter是通过MethodVisitor的visitCode()方法实现,onMethodExit是通过MethodVisitor的visitInsn(int opcode)方法实现

    3. 代码实现

    3.1 打印方法类

    package org.example.asm8.printArgs;
    
    public class PrintUtils {
    
        public static void printText(String str) {
            System.out.println(str);
        }
    
        public static void printObject(String name, Object value) {
            if (value == null) {
                System.out.println("null");
            } else {
                System.out.println(name + value.getClass().getSimpleName() + ",参数值:" + value);
            }
        }
    
        public static void printSpendTime(String methodName, long startTime) {
            System.out.println(methodName + " 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
            System.out.println("*************************************************");
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    3.2 字节码增强类

    package org.example.asm8.printArgs;
    
    import org.objectweb.asm.ClassReader;
    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.ClassWriter;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    import org.objectweb.asm.Type;
    import org.objectweb.asm.commons.AdviceAdapter;
    
    import java.io.FileOutputStream;
    
    public class PrintClassArgs implements Opcodes {
    
        public static void main(String[] args) throws Exception {
            ClassReader cr = new ClassReader(Hello.class.getName());
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
            cr.accept(new MyClassVisitor(ASM9, cw), ClassReader.EXPAND_FRAMES);
            byte[] bytes = cw.toByteArray();
            // 生成class
            String path = PrintClassArgs.class.getResource("/").getPath() + "org/example/asm8/printArgs/Hello.class";
            System.out.println("输出路径:" + path);
            try (FileOutputStream fos = new FileOutputStream(path)) {
                fos.write(bytes);
            }
    
        }
    
        static class MyClassVisitor extends ClassVisitor {
    
            protected MyClassVisitor(int api, ClassVisitor classVisitor) {
                super(api, classVisitor);
            }
    
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                String[] exceptions) {
                MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
                if (methodVisitor != null) {
                    // 排除抽象方法和本地方法
                    boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
                    boolean isNativeMethod = (access & ACC_NATIVE) != 0;
                    if (!isAbstractMethod && !isNativeMethod) {
                        methodVisitor = new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
                    }
                }
                return methodVisitor;
            }
        }
    
        static class MyMethodVisitor extends AdviceAdapter {
    
            String str = "方法名 " + super.getName() + ",方法描述符 " + super.methodDesc;
            // 开始时间在局部变量表中的位置
            int start = 0;
    
            protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
                super(api, methodVisitor, access, name, descriptor);
            }
    
            @Override
            protected void onMethodEnter() {
                // 记录开始时间
                super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                // 在局部变量表中nextLocal位置存放long类型的数值,nextLocal表示当前已存数据的下一个位置的索引
                // 下面执行完visitVarInsn(LSTORE, start)后,nextLocal会根据存入的数据类型长度,后移一位或两位
                start = nextLocal;
                // 将栈顶的数值放入局部变量表中start位置
                super.visitVarInsn(LSTORE, start);
                // 进入方法时,先打印一句话 Enter: xxx
                printText("Enter: " + str);
                // 取出方法所有的入参类型
                Type[] argumentTypes = getArgumentTypes();
                for (int i = 0; i < argumentTypes.length; i++) {
                    Type argumentType = argumentTypes[i];
                    // 将方法的入参从局部变量表中取出,压入到操作数栈中
                    loadArg(i);
                    // 对操作数栈顶的数据按照argumentType类型进行包装,并用包装好的值替换原来栈顶的这个数值,而且数据类型也是一致的
                    box(argumentType);
                    // 打印操作数栈顶的这个值,就实现了对方法入参的循环打印
                    printObject("入参类型:");
                }
    
            }
    
            @Override
            protected void onMethodExit(int opcode) {
                // 退出方法时,打印一句话 Exit: xxx
                printText("Exit: " + str);
                // throw 与 return 指令没有返回值,这里手动将希望打印到控制台的字符串压入到操作数栈顶
                if (opcode == ATHROW) {
                    super.visitLdcInsn("有异常抛出了");
                } else if (opcode == RETURN) {
                    super.visitLdcInsn("void方法,没有返回值");
                } else if (opcode == ARETURN) {
                    // 复制操作数栈顶的1个数值,并将复制结果压入操作数栈顶,此时操作数栈上有2个连续相同的数值
                    // 复制的目的是,多出来的这个数值用来打印到控制台,原来栈顶的数值不受影响
                    dup();
                } else if (opcode == LRETURN || opcode == DRETURN) {
                    // 因为double和long类型(64bit)占2个slot,所以要复制操作数栈顶的2个数值,并将其压入操作数栈顶
                    dup2();
                    // 对栈顶的数据按照返回值类型进行包装,并用包装好的值替换原来栈顶的这个数值
                    // double类型会用Double.valueOf()进行包装,long类型会用Long.valueOf()进行包装
                    box(getReturnType());
                } else {
                    dup();
                    // 这里排除上面几种返回值类型,这里的opcode应该是 FRETURN 和 IRETURN
                    // 对相应类型的数据进行Float.valueOf()或者Integer.valueOf()包装
                    box(getReturnType());
                }
                // 因为这里打印时,需要参数是Object类型,所以上面的2个box(getReturnType())必须有,目的是将基本数据类型转成包装类
                // 否则打印时,传的是基本数据类型,不是Object一定会报错
                // 前面2个if没有返回值,所以不需要按照返回值数据类型进行包装,直接传入String类型数据给printObject方法进行打印
                // 第3个if是Object类型返回值,复制一份压到栈顶即可,不需要再包装了
                printObject("返回值类型:");
                // 打印耗时
                printSpendTime();
            }
    
            private void printText(String str) {
                // 将str从常量池中取出,压入操作数栈顶
                super.visitLdcInsn(str);
                // 从操作数栈顶取出一个数据,作为入参调用PrintUtils的public static void printText(String str)方法
                super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrintUtils.class), "printText",
                    "(Ljava/lang/String;)V", false);
            }
    
            private void printObject(String name) {
                // 将name压入栈顶
                super.visitLdcInsn(name);
                // printObject方法入参是name和value,从栈顶取参数时,从后往前输入
                // 所以要先拿到Object类型的value再拿String类型的name,但此时栈顶是name,name下面是value的包装类
                // 所以要调用swap方法,将栈顶最顶端的两个数值互换(数值不能是long或double类型)
                swap();
                // 从操作数栈顶取出一个数据,作为入参调用PrintUtils的public static void printObject(String name, Object value)方法
                super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrintUtils.class), "printObject",
                    "(Ljava/lang/String;Ljava/lang/Object;)V", false);
            }
    
            private void printSpendTime() {
                // 方法名压入栈顶
                super.visitLdcInsn(super.getName());
                // 将开始时间从局部变量表start位置压入栈顶
                super.visitVarInsn(LLOAD, start);
                super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrintUtils.class), "printSpendTime",
                    "(Ljava/lang/String;J)V", false);
            }
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152

    3.3 执行增强

    • 执行main方法,查看被修改后的Hello.class,可以看出已经在方法进入和退出前添加了,打印入参、返回值和耗时的方法

    在这里插入图片描述

    3.4 验证

    • 测试类
    package org.example.asm8.printArgs;
    
    import java.util.Date;
    
    public class HelloRun {
    
        public static void main(String[] args) {
            Hello instance = new Hello("Fisher", 18, new Date());
            System.out.println(instance.testHello());
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 打印结果
    Enter: 方法名 ,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V
    入参类型:String,参数值:Fisher
    入参类型:Integer,参数值:18
    入参类型:Date,参数值:Wed Aug 17 10:04:08 CST 2022
    Exit: 方法名 ,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V
    返回值类型:String,参数值:void方法,没有返回值
     耗时:26 ms
    *************************************************
    Enter: 方法名 testHello,方法描述符 ()Ljava/lang/String;
    Enter: 方法名 hello,方法描述符 ()I
    Exit: 方法名 hello,方法描述符 ()I
    返回值类型:Integer,参数值:18
    hello 耗时:0 ms
    *************************************************
    Exit: 方法名 testHello,方法描述符 ()Ljava/lang/String;
    返回值类型:String,参数值:success!
    testHello 耗时:0 ms
    *************************************************
    success!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    4. 说明

    • 这里针对于box()方法进行说明

    在这里插入图片描述

    • 对于操作数栈顶数据是对象、数组和没有返回值的情况就不说了
    • 说下是基本数据类型时是怎么进行包装的
          Type boxedType = getBoxedType(type);
          newInstance(boxedType);
          if (type.getSize() == 2) {
            // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
            dupX2();
            dupX2();
            pop();
          } else {
            // p -> po -> opo -> oop -> o
            dupX1();
            swap();
          }
          invokeConstructor(boxedType, new Method("", Type.VOID_TYPE, new Type[] {type}));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 第一行是通过基本数据类型拿到对应的包装类,比如long->Long,int->Integer等
    2. 第二行是通过new关键字申请这个包装类的内存空间,然后将内存地址压入操作数栈顶,从注释可以到,不管是if中,还是else中,都在原有的Pp(表示是long或者double,占用2个slot)或者p(表示除了long和double之外的基本数据类型,占用1个slot)后面,变成了Ppo和po,表示在原来的栈顶多了一个o,这个o就是刚申请到的内存空间地址,可以为最后那行(invokeConstructor(boxedType, new Method(“”, Type.VOID_TYPE, new Type[] {type}));)在执行包装类对应的构造方法时使用
    3. 第三行如果size==2表示占用2个slot,说明是long或者double类型
    4. 第四行是对newInstance(boxedType);dupX2();dupX2();pop();invokeConstructor(boxedType, new Method(“”, Type.VOID_TYPE, new Type[] {type}));整个过程的注释
    5. 第五行是执行dup_x2指令将操作数栈顶(此时是Ppo)的数据复制一份,然后插入到距离栈顶3个slot位置下面(即变成oPpo)
    6. 第六行继续执行dup_x2指令将操作数栈顶(此时是oPpo)的数据复制一份,然后插入到距离栈顶3个slot位置下面(即变成ooPpo)
    7. 第七行是执行pop指令将操作数栈顶(此时是ooPpo)的数据弹出丢弃(即变成ooPp)
    8. 第八行表示是除了long和double之外的基本数据类型
    9. 第九行是对newInstance(boxedType);dupX1();swap();invokeConstructor(boxedType, new Method(“”, Type.VOID_TYPE, new Type[] {type}));整个过程的注释
    10. 第十行是执行dup_x1指令将操作数栈顶(此时是po)的数据复制一份,然后插入到距离栈顶2个slot位置下面(即变成opo)
    11. 第十一行是执行swap指令将操作数栈顶的2个数据进行交换(即变成oop)
    12. 第十二行是执行基本数据类型对应包装类的构造方法,需要先取出栈顶的基本数据类型的数值(即Pp或者p)用于初始化,再取出创建的包装类对象内存地址(即o)用于执行构造方法,用代码表示就是new Long(18L)或者new Integer(18),最后操作数栈顶的数值都会变为o,这个o就是对原来栈顶基本数据类型的数据进行包装后的包装类型数据,属于Object类型,就可以用于后面调用打印方法public static void printObject(String name, Object value) 时的这个value的输入
  • 相关阅读:
    程序设计部分 动态规划 习题
    Lakehouse 还是 Warehouse?(2/2).md
    新致转债上市价格预测
    【pytorch深度学习 应用篇02】训练中loss图的解读,训练中的问题与经验汇总
    图解LeetCode——782. 变为棋盘(难度:困难)
    MySQL 事务和锁
    golang——win10环境protobuf的使用
    中间件使用注意事项+中间件的分类
    百度地图实现3d发光建筑群
    轻量级日志系统——Loki
  • 原文地址:https://blog.csdn.net/qq_40977118/article/details/126378220