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!";
    }

}

在这里插入图片描述

  • 进行字节码插桩后达到下面效果
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: 方法名 <init>,方法描述符 (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: 方法名 <init>,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V");
        PrintUtils.printObject("返回值类型:", "void方法,没有返回值");
        PrintUtils.printSpendTime("<init>", 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!";
    }
}

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("*************************************************");
    }

}

在这里插入图片描述

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);
        }

    }

}

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());
    }

}

  • 打印结果
Enter: 方法名 <init>,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V
入参类型:String,参数值:Fisher
入参类型:Integer,参数值:18
入参类型:Date,参数值:Wed Aug 17 10:04:08 CST 2022
Exit: 方法名 <init>,方法描述符 (Ljava/lang/String;ILjava/util/Date;)V
返回值类型:String,参数值:void方法,没有返回值
<init> 耗时: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!

在这里插入图片描述

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("<init>", Type.VOID_TYPE, new Type[] {type}));
  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的输入
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值