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 ( ) ;
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 ) ;
}
}
}
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 ( ) ) ;
}
}
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. 说明
对于操作数栈顶数据是对象、数组和没有返回值的情况就不说了 说下是基本数据类型时是怎么进行包装的
Type boxedType = getBoxedType ( type) ;
newInstance ( boxedType) ;
if ( type. getSize ( ) == 2 ) {
dupX2 ( ) ;
dupX2 ( ) ;
pop ( ) ;
} else {
dupX1 ( ) ;
swap ( ) ;
}
invokeConstructor ( boxedType, new Method ( "" , 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的输入