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


    写在前面

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

  • 相关阅读:
    spdx-sbom-generator使用记录
    自定义RBAC(1)
    万字C语言之分支语句和循环语句
    Ansible如何使用lookup插件模板化外部数据
    验收材料-软件质量保证措施
    C/C++之(一)洛谷刷题及洛谷评测
    MFC Windows 程序设计[327]之表格控件例程三(附源码)
    若依(ruoyi-vue)后端部署windows系统
    node笔记记录35ES模块化规范3
    CentOS7安装MySQL8
  • 原文地址:https://blog.csdn.net/wang0907/article/details/140044611