写在前面
源码 。
本文看下通过ASM如何实现插桩调用其他类的静态方法。
1:编码
假定有如下的类:
public class PayController {
public void pay(int userId, int payAmount) {
System.out.println("用户:" + userId + ", 调用支付系统完成支付" + payAmount + ",准备发货!");
return;
}
}
现在呢,假定有如下的日志审计类,用来记录日志信息:
/**
* 日志审计工具类
*/
public class AuditLogUtil {
public static void infoLog(String funcName, int... params) {
System.out.println("方法:" + funcName + ", 参数:" + "[" + params[0] + "," + params[1] + "]");
}
}
现在有一个需求,需要在调用PayController#pay方法时,增加审计日志的记录,也就是像下面这样的代码:
public class PayController {
public void pay(int userId, int payAmount) {
AuditLogUtil.infoLog("pay", new int[] {userId, payAmount});
System.out.println("用户:" + userId + ", 调用支付系统完成支付" + payAmount + ",准备发货!");
return;
}
}
但是,该需求并不是一直有的,也就最近半年需要,如果硬编码来做,显然不是一个很好的方案,所以啊,使用插桩再结合javaagent来实现,就是很好的方案了!本部分就来看下如何进行插桩,直接来看代码吧:
package com.dahuyou.asm.callOuterCls;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.AdviceAdapter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import static org.objectweb.asm.Opcodes.ASM5;
public class CallOuterMethodEnhancer extends ClassLoader {
public static void main(String[] args) throws Exception {
// 读取要插桩加强的类
ClassReader cr = new ClassReader(PayController.class.getName());
// 准备往要插桩加强的类中写内容
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
// 准备插桩
ClassVisitor cv = new ProfilingClassAdapter(cw, PayController.class.getSimpleName());
// 正式插桩
cr.accept(cv, ClassReader.EXPAND_FRAMES);
// 获取插桩后的代码
byte[] bytes = cw.toByteArray();
// 反射执行插桩后的字节码
Class<?> clazz = new CallOuterMethodEnhancer().defineClass("com.dahuyou.asm.callOuterCls.PayController", bytes, 0, bytes.length);
// 反射获取 main 方法
Method method = clazz.getMethod("pay", int.class, int.class);
Object obj = method.invoke(clazz.newInstance(), 69089, 285);
System.out.println("结果:" + obj);
outputClazz(bytes);
}
static class ProfilingClassAdapter extends ClassVisitor {
public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {
super(ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (!"pay".equals(name)) return super.visitMethod(access, name, descriptor, signature, exceptions);
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
return new ProfilingMethodVisitor(mv, access, name, descriptor);
}
}
static class ProfilingMethodVisitor extends AdviceAdapter {
private String name;
protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(ASM5, methodVisitor, access, name, descriptor);
this.name = name;
}
@Override
public void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
super.visitFieldInsn(opcode, owner, name, descriptor);
}
/**
* 实现效果:
* public void pay(int userId, int payAmount) {
* AuditLogUtil.infoLog("pay", new int[] {userId, payAmount});
* System.out.println("用户:" + userId + ", 调用支付系统完成支付" + payAmount + ",准备发货!");
* return;
* }
* 其中AuditLogUtil.infoLog("pay", new int[] {userId, payAmount});就是要插桩的代码
*/
@Override
protected void onMethodEnter() {
// ldc 加载方法名称常量
mv.visitLdcInsn(name); // 方法名称压到栈顶 此时栈:pay
mv.visitInsn(ICONST_2); // 将int型2推送至栈顶 此时栈:2, pay
mv.visitIntInsn(NEWARRAY, T_INT); // 获取栈顶元素,并以其为长度创建一个数组,并将其引用压倒栈顶 此时栈:new int[]{}, pay
mv.visitInsn(DUP); // 复制栈顶元素并压到栈顶 此时栈:new int[], new int[], pay
mv.visitInsn(ICONST_0); // 将常量0压到栈顶 此时栈:0, new int[], new int[], pay
mv.visitVarInsn(ILOAD, 1); // 将本地变量表1位置变量压倒栈顶 1位置变量, 此时栈:0, new int[], new int[], pay
mv.visitInsn(IASTORE); // 将栈顶int型数值存入指定数组的指定索引位置 new int[0] = 1位置变量,此时栈new int[], pay
mv.visitInsn(DUP); // 复制栈顶元素 此时栈:new int[], new int[], pay
mv.visitInsn(ICONST_1); // 加载常量1 此时栈:1, new int[], new int[], pay
mv.visitVarInsn(ILOAD, 2); // 加载本地变量表slot 2变量 此时栈:2位置变量, 1, new int[], new int[], pay
mv.visitInsn(IASTORE); // 栈顶元素存储到数组 new int[1] = 2位置变量 此时栈:new int[], pay
mv.visitMethodInsn(INVOKESTATIC, "com/dahuyou/asm/callOuterCls/AuditLogUtil", "infoLog", "(Ljava/lang/String;[I)V", false); // 调用静态方法infoLog,参数为当前栈的new int[], pay,完成打印
}
}
private static void outputClazz(byte[] bytes) {
// 输出类字节码
FileOutputStream out = null;
try {
String pathName = CallOuterMethodEnhancer.class.getResource("/").getPath() + "AsmCallOuterMethodEnhancer.class";
out = new FileOutputStream(new File(pathName));
System.out.println("ASM类输出路径:" + pathName);
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码比较长,其中比较关键代码为:
ClassReader cr = new ClassReader(PayController.class.getName());
读取要插装增强的类准备插装
ClassVisitor cv = new ProfilingClassAdapter(cw, PayController.class.getSimpleName());
进行插装,具体是在com.dahuyou.asm.methodWasteTime.TestMonitor.ProfilingClassAdapter#visitMethod中返回自定义的methodvisitor实现插装
static class ProfilingMethodVisitor extends AdviceAdapter
methovisitor插装切面类,onMethodEnter方法插装方法执行前的逻辑,onMethodExit插装方法执行后的逻辑
byte[] bytes = cw.toByteArray();
这就拿到插装后的字节码了
主要看法方法onMethodEnter,完成了插桩代码AuditLogUtil.infoLog("pay", new int[] {userId, payAmount});
,已经写了比较详细的注释,还有哪里看不懂的话,就留言告诉我。
接着运行测试:
为了更加清晰,看下生成的插桩后的字节码:
写在后面
参考文章列表
JVM 虚拟机字节码指令表 。