字节码编程ASM之插桩调用其他类的静态方法

发布于:2024-06-28 ⋅ 阅读:(49) ⋅ 点赞:(0)

写在前面

源码
本文看下通过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 虚拟机字节码指令表


网站公告

今日签到

点亮在社区的每一天
去签到