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("*************************************************");
}
}
![](https://img-blog.csdnimg.cn/direct/a250ad1342cd46298cbb9b6ebf60e287.png)
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,可以看出已经在方法进入和退出前添加了,打印入参、返回值和耗时的方法
![](https://img-blog.csdnimg.cn/direct/2d6739d4280641468f3d53722df74871.png)
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}));
- 第一行是通过基本数据类型拿到对应的包装类,比如long->Long,int->Integer等
- 第二行是通过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}));)在执行包装类对应的构造方法时使用
- 第三行如果size==2表示占用2个slot,说明是long或者double类型
- 第四行是对newInstance(boxedType);dupX2();dupX2();pop();invokeConstructor(boxedType, new Method(“”, Type.VOID_TYPE, new Type[] {type}));整个过程的注释
- 第五行是执行dup_x2指令将操作数栈顶(此时是Ppo)的数据复制一份,然后插入到距离栈顶3个slot位置下面(即变成oPpo)
- 第六行继续执行dup_x2指令将操作数栈顶(此时是oPpo)的数据复制一份,然后插入到距离栈顶3个slot位置下面(即变成ooPpo)
- 第七行是执行pop指令将操作数栈顶(此时是ooPpo)的数据弹出丢弃(即变成ooPp)
- 第八行表示是除了long和double之外的基本数据类型
- 第九行是对newInstance(boxedType);dupX1();swap();invokeConstructor(boxedType, new Method(“”, Type.VOID_TYPE, new Type[] {type}));整个过程的注释
- 第十行是执行dup_x1指令将操作数栈顶(此时是po)的数据复制一份,然后插入到距离栈顶2个slot位置下面(即变成opo)
- 第十一行是执行swap指令将操作数栈顶的2个数据进行交换(即变成oop)
- 第十二行是执行基本数据类型对应包装类的构造方法,需要先取出栈顶的基本数据类型的数值(即Pp或者p)用于初始化,再取出创建的包装类对象内存地址(即o)用于执行构造方法,用代码表示就是new Long(18L)或者new Integer(18),最后操作数栈顶的数值都会变为o,这个o就是对原来栈顶基本数据类型的数据进行包装后的包装类型数据,属于Object类型,就可以用于后面调用打印方法public static void printObject(String name, Object value) 时的这个value的输入