字节码编程ASM之插桩方法调用记录

写在前面

源码 。
正式开始之前,先分享一个让人”悲伤“的真实的故事。
那是一个风和日丽的周六的下午,俺正在开开心心的打着羽毛球,突然接到了来自于最不想联系的那个人(没错,这个人就是我的领导!!!)的微信语音,语音里领导说线上出现问题:工作流任务办理,点击办理,不出办理人了,你到明天去公司加班看下咋回事吧!。瞬间整个人都不好了啊。

事后复盘导致bug出现的原因是测试场景覆盖的不够全导致(不要脸,代码写的有问题,就知道推给可爱的测试同学),也就是导致出现问题的方法在测试环境压根就没有调用到过。

而且因为这种原因导致的线上bug,已然不是第一次遇到了,所以我就在想,有没有什么办法能够规避这种问题的发生呢?测试用例每次都会投入大量的时间和测试同学一起评审啊!还是会出现场景覆盖不全的问题。头痛医头,脚痛医脚,既然是方法没有调用到,那我们只要在方法调用的时候打个日志记录下,测试后看下哪个方法没有日志不就是没有调用到吗!但额外的去写这种侵入业务的代码感觉不是太好,最后,想到了字节码插桩技术,只要在方法执行前插入一行日志打印代码,再和javaagent结合在一起使用不就行了,对程序完全无侵入。

嗯,完美的方案!!!

1:ASM版本

我们一步步来,首先要搞清楚程序的执行顺序,然后再搞清楚核心步骤都做了哪些事情,最后最好再自己手撕一遍。
首先我们要来定义premain,这是javaagent执行的入口:

/**
 * premain 程序,清单文件中需要配置的入口类
 */
public class PreMain {
    //JVM 首先尝试在代理类上调用以下方法
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new MethodRecordingTransformer());
    }

    //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
    public static void premain(String agentArgs) {
    }
}

类MethodRecordingTransformer是自定义的转换器,通过addTransformer方法注册到Instrument对象,这个转换器实现了转换规范接口java.lang.instrument.ClassFileTransformer,用来将字节码从一种形式转换为另外一种形式(这个过程就是插桩了)。如下:

public class MethodRecordingTransformer implements ClassFileTransformer {

    /**
     * 所有的类被来加载器加载的时候都会走这个方法,这样我们就有机会通过各种技术手段来对字节码进行插桩了
     *
     * @param loader
     * @param className           类全限定名称
     * @param classBeingRedefined
     * @param protectionDomain
     * @param classfileBuffer     类字节码对应的二进制数组,可以通过defineClass直接加载到JVM中并生成Class对象
     * @return
     * @throws IllegalClassFormatException
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            // 过滤不需要插桩的类
            if (MethodRecordingFilter.isNotNeedInject(className)) {
                return classfileBuffer;
            }
            // 插桩
            byte[] bytes = generateInstrumentationCode(loader, className, classfileBuffer);
            // 写到磁盘的.class文件中,方便调试
            outputClazz(bytes, className);
            return bytes;
        } catch (Throwable e) {
            System.out.println(e.getMessage());
        }
        return classfileBuffer;

    }

    private void outputClazz(byte[] bytes, String className) {
        // 输出类字节码
        FileOutputStream out = null;
        try {
            String pathName = MethodRecordingTransformer.class.getResource("/").getPath() + className + "SQM.class";
            out = new FileOutputStream(new File(pathName));
            System.out.println("插桩后代码输出路径:" + pathName);
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 生成插桩代码
     *
     * @param loader
     * @param className
     * @param classfileBuffer
     * @return
     */
    private byte[] generateInstrumentationCode(ClassLoader loader, String className, byte[] classfileBuffer) {
        // 读取原有类
        ClassReader cr = new ClassReader(classfileBuffer);
        // 通过writer向需要插桩类写入插桩逻辑
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        // 使用子自定义的类访问器来定义插桩逻辑
        ClassVisitor cv = new MethodRecordingClassAdapter(cw, className);
        // 真正插桩
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }
}

主要看法方法generateInstrumentationCode,用来生成插桩后的二进制字节码文件,也就是真正被加载到JVM中运行的代码。generateInstrumentationCode方法中使用MethodRecordingClassAdapter,这是自定义的ClassVisitor的子类,用来进行判断是否需要过滤等操作,如下:

public class MethodRecordingClassAdapter extends ClassVisitor {
    // snip code

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        // 不对接口和私有方法注入
        if (isInterface || (access & ACC_PRIVATE) != 0) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }
        //不对抽象方法、native方法、桥接方法、合成方法进行注入
        if ((access & ACC_ABSTRACT) != 0
                || (access & ACC_NATIVE) != 0
                || (access & ACC_BRIDGE) != 0
                || (access & ACC_SYNTHETIC) != 0) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }
        if ("<init>".equals(name) || "<clinit>".equals(name)) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }
        // 过滤Object类默认方法
        if (MethodRecordingFilter.isNotNeedInjectMethod(name)) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }
        // 获取要增强方法的MethodVisitor对象
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if (null == mv) return null;
        // 使用扩展的MethodVisitor来对方法进行插桩
        return new MethodRecordingVisitorAdvice(access, name, descriptor, mv, className, fullClazzName, simpleClassName);
    }
}

主要看方法visitMethod最终返回的MethodVisitor类型的对象MethodRecordingVisitorAdvice,定义了方法执行前以及方法返回前要执行的逻辑:

public class MethodRecordingVisitorAdvice extends AdviceAdapter {
    private final String className;
    private String methodName;

    protected MethodRecordingVisitorAdvice(int access, String methodName, String desc, MethodVisitor mv, String className, String fullClassName, String simpleClassName) {
        super(ASM5, mv, access, methodName, desc);
        this.methodName = methodName;
        this.className = className;
    }

    @Override
    protected void onMethodEnter() {
        // public static void printLog(String className, String methodName) {
        mv.visitLdcInsn(className);
        mv.visitLdcInsn(methodName);
        mv.visitMethodInsn(INVOKESTATIC, "com/dahuyou/method/record/LogUtil", "printLog", "(Ljava/lang/String;Ljava/lang/String;)V", false);
    }

    /**
     * 方法执行结束前调用,即return,throw异常前调用
     *
     * @param opcode 操作码 org.objectweb.asm.Opcodes 比如方法的public private啊,jdk的版本号啊,位运算啊,各种字节码指令啊当然包含这个方法会传入的如下可能的操作码:
     * int IRETURN = 172; // 返回int
     * int LRETURN = 173; // 返回long
     * int FRETURN = 174; // 返回float
     * int DRETURN = 175; // 返回double
     * int ARETURN = 176; // 返回引用类型
     * int RETURN = 177; // 返回void
     * 这里就可以通过opcode知道方法返回的值是什么了!
     */
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
    }
}

这里只对onMethodEnter方法添加了插桩代码,即方法执行前调用LogUtil的静态方法printLog:

public class LogUtil {
    public static void printLog(String className, String methodName) {
        // 实际场景根据需要就可以写到日志文件,或者是对接到消息中间件了
        System.out.println("类:[" + className + "], 方法:[" + methodName + "] 被测试同学覆盖到了!");
    }
}

这里只是模拟了,实际场景根据具体情况修改LogUtil类就行了。

然后我们就可以来测试了,首先来打包:
在这里插入图片描述
准备测试代码:

/*
获取工作流任务办理人的工具类
 */
public class WorkflowUtil {
    public List<String> queryAssignee() {
        return null;
    }
}

public class Tst {
    public static void main(String[] args) {

        new WorkflowUtil().queryAssignee();
    }
}

接着还要配置javaagent:
在这里插入图片描述
最后运行:
在这里插入图片描述
插桩后的字节码为:
在这里插入图片描述

2:javaassit 版本 TODO

2:ByteBuddy 版本 TODO

写在后面

参考文章列表

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

字节码编程ASM之helleworld 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/769089.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT Creator生成uml类图

先说方法&#xff0c;使用Doxygen工具&#xff0c;笔者用的虚拟机linux系统下的qt5.7&#xff0c;没找到自带的uml生成类的工具。 1、Doxygen 安装 在 Ubuntu 系统中&#xff0c;执行下面命令安装 doxygen 和 graphviz 软件包。 sudo apt install graphviz # 用于生成代码…

等保2.0 实施方案之信息软件验证要求

一、等保2.0背景及意义 随着信息技术的快速发展和网络安全威胁的不断演变&#xff0c;网络安全已成为国家安全、社会稳定和经济发展的重要保障。等保2.0&#xff08;即《信息安全技术 网络安全等级保护基本要求》2.0版本&#xff09;作为网络安全等级保护制度的最新标准&#x…

Gradle学习-5 发布二进制插件

注&#xff1a;以下示例基于Gradle8.0 1、发布插件 复制一分 buildSrc&#xff0c;执行命令行&#xff0c;生成一个新目录 leon-gradle-plugin cp -rf buildSrc leon-gradle-plugin在 leon-gradle-plugin 目录下的 build.gradle 中引入maven plugins{// 引用 Groovy 插件&…

【热部署】✈️Springboot 项目的热部署实现方式

目录 &#x1f378;前言 &#x1f37b;一、热部署和手动重启 &#x1f37a;二、热部署的实现 2.1 手动启动热部署 2.2 自动检测热部署 2.3 关闭热部署 &#x1f49e;️三、章末 &#x1f378;前言 小伙伴们大家好&#xff0c;书接上文&#xff0c;通过Springboot 中的 actu…

解析Kotlin中扩展函数与扩展属性【笔记摘要】

1.扩展函数 1.1 作用域&#xff1a;扩展函数写的位置不同&#xff0c;作用域就也不同 扩展函数可以写成顶层函数&#xff08;Top-level Function&#xff09;&#xff0c;此时它只属于它所在的 package。这样你就能在任何类里使用它&#xff1a; package com.rengwuxianfun …

zabbix“专家坐诊”第244期问答

问题一 Q&#xff1a;请教一下&#xff0c;我的zabbix6.0配置的基于snmptrap上报的日志提取关键字推送告警&#xff0c;正则表达式能否帮忙看看怎么弄&#xff1f;我这配置的提示一直不正确&#xff1f; A&#xff1a;具体看一下这里的信息。 Q&#xff1a;这个我是直接复制的…

如何计算弧线弹道的落地位置

1&#xff09;如何计算弧线弹道的落地位置 2&#xff09;Unity 2021 IL2CPP下使用Protobuf-net序列化报异常 3&#xff09;编译问题&#xff0c;用Mono可以&#xff0c;但用IL2CPP就报错 4&#xff09;Wwise的Bank在安卓上LoadBank之后&#xff0c;播放没有声音 这是第393篇UWA…

ssm旅游信息分享网站-计算机毕业设计源码92194

目录 1 绪论 1.1 研究背景 1.2研究意义 1.3论文结构与章节安排 2 旅游信息分享网站分析 2.1 可行性分析 2.2 系统功能分析 2.3 系统用例分析 2.4 系统流程分析 2.5本章小结 3 旅游信息分享网站总体设计 3.1 系统功能模块设计 3.2 数据库设计 3.4本章小结 4 旅游信…

从全连接到卷积

一、全连接到卷积 1、卷积具有两个原则&#xff1a; 平移不变性&#xff1a;无论作用在哪个部分&#xff0c;它都要有相同的作用&#xff0c;而不会随着位置的改变而改变 局部性&#xff1a;卷积核作用处&#xff0c;作用域应该是核作用点的周围一小部分而不作用于更大的部分 …

一篇文章搞懂弹性云服务器和轻量云服务器的区别

前言 在众多的云服务器类型中&#xff0c;弹性云服务器和轻量云服务器因其各自的特点和优势&#xff0c;受到了广大用户的青睐。那么&#xff0c;这两者之间到底有哪些区别呢&#xff1f;本文将为您详细解析。 弹性云服务器&#xff1a;灵活多变的计算资源池 弹性云服务器&…

【计算机体系结构】缓存的false sharing

在介绍缓存的false sharing之前&#xff0c;本文先介绍一下多核系统中缓存一致性是如何维护的。 目前主流的多核系统中的缓存一致性协议是MESI协议及其衍生协议。 MESI协议 MESI协议的4种状态 MESI协议有4种状态。MESI是4种状态的首字母缩写&#xff0c;缓存行的4种状态分别…

CoAtNet(NeurIPS 2023, Google)论文解读

paper&#xff1a;CoAtNet: Marrying Convolution and Attention for All Data Sizes third-party implementation&#xff1a;https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/maxxvit.py 背景 自AlexNet以来&#xff0c;ConvNets一直是计算机…

【接口自动化测试】第四节.实现项目核心业务的单接口自动化测试

文章目录 前言一、登录单接口自动化测试 1.1 登录单接口文档信息 1.2 登录成功 1.3 登录失败&#xff08;用户名为空&#xff09;二、数据驱动的实现 2.1 json文件实现数据驱动三、课程添加单接口自动化测试 3.1 课程添加单接口文档信息 3.2 课程…

N5 使用Gensim库训练Word2Vec模型

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊# 前言 前言 这周学习训练一个Word2Vec模型&#xff0c;并进行一些基本的词向量操作。 Word2Vec 模型 Word2Vec 是一种基于神经网络的词向量表示方法&#x…

Qt Q_ASSERT详解

Q_ASSERT详解 引言一、基本用法二、深入了解三、参考链接 引言 Q_ASSERT是 Qt 框架中的一个宏&#xff0c;用于在调试时检查某个条件是否为真。它是程序调试中的一个重要工具&#xff0c;有助于开发者在开发过程中及时发现并修复潜在的错误。 一、基本用法 只在使用 Qt 的 D…

API 授权最佳实践

API&#xff08;应用程序编程接口&#xff09;就像秘密之门&#xff0c;允许不同的软件程序进行通信。但并不是每个人都应该拥有每扇门的钥匙&#xff0c;就像不是每个软件都应该不受限制地访问每个 API 一样。 这些 API 将从银行的移动应用程序到您最喜欢的社交媒体平台的所有…

嵌入式C语言中指针与链表的关系详解

假定给你一块非常小的内存,这块内存只有8字节,这里也没有高级语言,没有操作系统,你操作的数据单位是单个字节,你该怎样读写这块内存呢? 注意这里的限定,再读一遍,没有高级语言,没有操作系统,在这样的限制之下,你必须直面内存读写的本质。 这个本质是什么呢? 本质…

Vuex的基本使用

1.安装vuex npm i vuex3 2.引入 import Vuex from vuex 3.使用 Vue.use(Vuex) 4.在src下的目录创建store&#xff0c;新建index.js import store from ./store 5.编写index.js import Vue from vue import Vuex from vuex Vue.use(Vuex)//用于操作组件中的动作 const actions{a…

Linux安装Node-RED并实现后台运行及开机启动

首先确保系统中已近成功安装Node.js&#xff0c;并保证需要的合适版本&#xff1a; 关于node.js的安装可以参考我的另一篇博文:《AliyunOS安装Node.js》。 然后就可以使用npm工具安装Node-RED了&#xff0c;很简单使用如下命令&#xff1a; sudo npm install -g --unsafe-per…

antd Select前端加模糊搜索

背景&#xff1a;前端的小伙伴经常在开发antd Select的时候后端不提供搜索模糊搜索接口&#xff0c;而是全量返回数据&#xff0c;这个时候就需要我们前端自己来写一个模糊搜索了。 效果 代码截图 代码 <SelectshowSearchmode"multiple"options{studioList}filte…