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 ( ) ;
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 ) ;
start = nextLocal;
super . visitVarInsn ( LSTORE, start) ;
printText ( "Enter: " + str) ;
Type [ ] argumentTypes = getArgumentTypes ( ) ;
for ( int i = 0 ; i < argumentTypes. length; i++ ) {
Type argumentType = argumentTypes[ i] ;
loadArg ( i) ;
box ( argumentType) ;
printObject ( "入参类型:" ) ;
}
}
@Override
protected void onMethodExit ( int opcode) {
printText ( "Exit: " + str) ;
if ( opcode == ATHROW) {
super . visitLdcInsn ( "有异常抛出了" ) ;
} else if ( opcode == RETURN) {
super . visitLdcInsn ( "void方法,没有返回值" ) ;
} else if ( opcode == ARETURN) {
dup ( ) ;
} else if ( opcode == LRETURN || opcode == DRETURN) {
dup2 ( ) ;
box ( getReturnType ( ) ) ;
} else {
dup ( ) ;
box ( getReturnType ( ) ) ;
}
printObject ( "返回值类型:" ) ;
printSpendTime ( ) ;
}
private void printText ( String str) {
super . visitLdcInsn ( str) ;
super . visitMethodInsn ( INVOKESTATIC, Type . getInternalName ( PrintUtils . class ) , "printText" ,
"(Ljava/lang/String;)V" , false ) ;
}
private void printObject ( String name) {
super . visitLdcInsn ( name) ;
swap ( ) ;
super . visitMethodInsn ( INVOKESTATIC, Type . getInternalName ( PrintUtils . class ) , "printObject" ,
"(Ljava/lang/String;Ljava/lang/Object;)V" , false ) ;
}
private void printSpendTime ( ) {
super . visitLdcInsn ( super . getName ( ) ) ;
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. 说明
对于操作数栈顶数据是对象、数组和没有返回值的情况就不说了 说下是基本数据类型时是怎么进行包装的
Type boxedType = getBoxedType ( type) ;
newInstance ( boxedType) ;
if ( type. getSize ( ) == 2 ) {
dupX2 ( ) ;
dupX2 ( ) ;
pop ( ) ;
} else {
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的输入