news 2026/9/23 10:18:30

IntelliJ IDEA 插件与 Byte Buddy 字节码插桩:在 CodeGuide 中落地研发交付质量自动分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
IntelliJ IDEA 插件与 Byte Buddy 字节码插桩:在 CodeGuide 中落地研发交付质量自动分析

IntelliJ IDEA 插件与 Byte Buddy 字节码插桩:在 CodeGuide 中落地研发交付质量自动分析

【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide

本文以 CodeGuide 仓库中「《IntelliJ IDEA 插件开发》第10节:基于字节码插桩采集数据,实现代码交付质量自动分析」为核心骨架,结合仓库内 Byte Buddy 系列、JavaAgent 系列与 IDEA 插件探针系列文档,完整还原一套"研发编码过程即采集、字节码插桩即增强、服务端汇总即评估"的交付质量分析方案。读者读完将掌握:如何用premain入口与-javaagent挂载探针、如何用 Byte Buddy 的@Origin/@SuperCall/@AllArguments注解采集方法出入参与耗时、如何通过继承DefaultJavaProgramRunnerJavaProgramPatcher让 IDEA 插件在"启动运行"时自动注入探针,最终把代码质量评估从"提测后置"提前到"开发过程中"。

一、背景:研发交付质量为什么要"前置评估"

业务提需求、产品定方案、研发做实现、测试验流程,四个角色的相互配合是确保一个需求上线的必备条件。在整个需求的交付质量级别划分中,研发与测试是非常重的一环——如果研发提测的代码质量不高,就会出现不同级别的修 BUG、返工甚至重做的风险。

那么,怎么来提高代码质量呢?一般我们都会要求研发在开发代码的过程中编写单元测试,验证自己的代码逻辑。如果最终单元测试覆盖度不足,可以由测试拒绝研发提测。

但是,整个需求实现的代码是在全部开发完成后提测的,也就是临近上线的最后一环,大家才知道某个研发的某个功能域的实现是否具备提测条件。如果这个时候代码质量不高,那么接下来就是项目风险的时候——压测试时间、调上线时间,总之"有病拖着最后成大病了"。

当然,你可以在项目开发期间定期排查代码,或者通过日会进度反馈等手段。但这样需要耗费大量时间的 1 对 1 人工排查方式,很难满足复杂流程的较大型项目开发,而且对项目风险把控也是不可预估的。

所以,我们希望采集研发在开发过程中的执行动作,把风险判断提前。实际操作举例就是:当你开发完成一个接口、开始测试运行时,插件就可以采集到这个接口的全部信息,包括接口名称、入参类型和内容、出参类型和内容、异常信息、调用关系链等。这些信息汇总提交到服务端后,可以生成本次需求代码分支下的全部接口动作,以及各系统间的关系链路,并附带随时生成最新的接口文档和一键测试验证功能。后期测试人员介入时就可以参考研发在编码过程中的全部测试用例,也可以查看整个功能的覆盖程度,测试人员测试过程中的数据同样会被保留。有了这些数据,就能完整生成一套"研发测试质量交付全览图",让整个工程开发交付质量评估透明化。

这就是本文要实践的目标:把代码质量检查从"提测后"前移到"编码中"

二、需求目的与总体技术选型

要实现上述目标,需要三项核心技术配合:

  1. 字节码插桩:因为要采集到接口执行信息,就需要使用字节码插桩组件给接口方法增强。这个实现有点类似谷歌 Dapper 大规模分布式架构的非入侵监控,只不过这里需要采集的描述性信息更多。关于字节码插桩,可以了解 ASM、Javassist、Byte-Buddy,它们都可以做此项工作。
  2. IDEA 插件开发:要在研发人员开发过程中进行采集、同时不破坏研发的操作习惯,最好的方式就是嵌入到「启动运行」中——只要在开发过程中有运行代码的动作,就采集相应的接口信息。
  3. 数据传输与处理:传输可以使用 MQ 或者直接用 Netty;处理数据的过程相对比较复杂,需要分析出有价值的数据、把同类数据合并成一条执行链路的数据,并生成相关的接口文档和工程服务地图。

围绕这三项技术,仓库内的相关专题恰好形成一条完整的学习链路:

  • 面经手册 · 第13篇《除了JDK、CGLIB,还有3种类代理方式?》:铺垫代理/增强技术全家谱;
  • 基于JavaAgent的全链路监控系列:讲清premain-javaagent的底层机制;
  • 字节码编程,Byte-buddy 系列:掌握 Byte Buddy 的委托式插桩 API;
  • 《IntelliJ IDEA 插件开发》第8节:在插件中引入探针:一个同构的落地案例(采集执行 SQL);
  • 本文要讲解的 第10节:基于字节码插桩采集数据,实现代码交付质量自动分析 以及其在中间件系列中的姊妹篇 第 18 章:采集研发过程中代码执行信息。

三、字节码插桩技术选型:为什么用 Byte Buddy

这里使用的字节码插桩组件是Byte Buddy。它是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

它的核心优势(原文档结论):

  • 无需理解字节码指令,即可使用简单的 API 很容易地操作字节码,控制类和方法;
  • 已支持 Java 11,库轻量,仅依赖 Java 字节码解析器库 ASM 的访问者 API,本身不需要其他任何依赖项;
  • 比起 JDK 动态代理、CGLIB、Javassist,Byte Buddy 在性能上具有一定的优势。

从仓库内 Byte-buddy 篇二《监控方法执行耗时动态获取出入参类型和值》 的实践可以看到,用 Byte Buddy 监控一个业务方法只需要三步:

  1. 通过ByteBuddy().subclass(...)+ElementMatchers.named("方法名")定位目标方法;
  2. 通过MethodDelegation.to(MonitorDemo.class)把方法执行委托给监控类;
  3. 在监控类中使用@RuntimeType@Origin@SuperCall等注解获取方法、调用链与耗时信息。

也就是说,Byte Buddy 的使用方式"有些像使用 AOP 的拦截方式",把最核心的字节码增强逻辑封装成高级 API,让开发者把精力放在"采集什么信息"而非"如何改写字节码"上。

常用注解速查表

在编写监控方法时,Byte Buddy 提供了一系列注解用于绑定不同的上下文信息。下表整理自仓库 Byte-buddy 篇二文档:

注解说明
@Argument绑定单个参数
@AllArguments绑定所有参数的数组
@This当前被拦截的、动态生成的那个对象
@Super当前被拦截的、动态生成的那个对象的父类对象
@Origin可绑定到Method(被调用的原始方法)、ConstructorClassMethodHandleMethodTypeString(动态类的toString())、int(动态方法的修饰符)等类型
@DefaultCall调用默认方法而非 super 的方法
@SuperCall调用父类版本的方法
@RuntimeType用于返回值、参数上,提示 Byte Buddy 禁用严格的类型检查
@Empty注入参数类型的默认值
@StubValue注入一个存根值:对返回引用类型、void 的方法注入null;对返回原始类型的方法注入0
@FieldValue注入被拦截对象的一个字段的值
@Morph类似于@SuperCall,但允许指定调用参数

四、探针工程:从 premain 入口到方法信息采集

1. 方法入口:premain

如果你接触过 JavaAgent 开发,对premain会比较熟悉。可以把premain理解为程序启动时的方法入口,你可以从这个入口中拦截到你需要的方法,之后对它进行字节码增强——其实也就是动态写代码,在方法中插入你的代码来收集方法信息。

public static void premain(String agentArgs, Instrumentation inst) { AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> { return builder .method(ElementMatchers.any()) // 拦截任意方法 .intercept(MethodDelegation.to(MonitorMethod.class)); }; new AgentBuilder .Default() .type(ElementMatchers.nameStartsWith(agentArgs)) .transform(transformer) .installOn(inst); }

这里的两个关键点:

  • .type(ElementMatchers.nameStartsWith(agentArgs)):只对以传入的包名前缀开头的类做增强。在本文场景中,agentArgs正是从 IDEA 侧传入的当前源码包名,实现"只采集本项目代码、不干扰框架类"的精准控制;
  • .method(ElementMatchers.any()).intercept(MethodDelegation.to(MonitorMethod.class)):拦截匹配类的任意方法,并把方法执行委托给MonitorMethod这个监控类处理。

关于premain-javaagent的底层机制,仓库的 基于JavaAgent的全链路监控一《嗨!JavaAgent》 给出了最简可运行示例:JVM 首先尝试在代理类上调用premain(String agentArgs, Instrumentation inst);如果代理类没有实现这个重载,JVM 将尝试调用premain(String agentArgs)。同时需要把代理入口配置进MANIFEST.MF

Manifest-Version: 1.0 Premain-Class: org.itstack.demo.agent.MyAgent Can-Redefine-Classes: true

再通过Run/Debug Configurations -> VM options配置-javaagent:探针Jar绝对路径=参数即可在程序启动时进入探针入口。这与本文案例中 IDEA 插件自动拼接-javaagent参数的思路完全一致。

2. 采集信息:拦截方法与出入参

使用 Byte Buddy 可以采集到一个方法的全部信息:方法名称、入参个数、入参类型和内容、出参类型和结果,以及方法执行耗时。核心代码如下:

@RuntimeType public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception { long start = System.currentTimeMillis(); Object resObj = null; try { resObj = callable.call(); return resObj; } finally { System.out.println("方法名称:" + method.getName()); System.out.println("入参个数:" + method.getParameterCount()); for (int i = 0; i < method.getParameterCount(); i++) { System.out.println("入参 Idx:" + (i + 1) + " 类型:" + method.getParameterTypes()[i].getTypeName() + " 内容:" + args[i]); } System.out.println("出参类型:" + method.getReturnType().getName()); System.out.println("出参结果:" + resObj); System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms"); } }

这段代码可以逐层拆解:

  • @RuntimeType:运行时返回类型标注,允许拦截方法与原方法签名存在差异时仍能正确绑定;
  • @Origin Method method:拿到被调用的原始方法对象,从而获取方法名、参数个数、参数类型、返回类型等描述信息;
  • @SuperCall Callable<?> callable:调用父类(原方法)版本的执行逻辑——callable.call()即执行真实业务方法,返回值原样返回,不改变业务行为;
  • @AllArguments Object[] args:拿到本次调用的全部实参,配合method.getParameterTypes()即可输出每个入参的"类型 + 内容";
  • 整个采集放在finally块中,保证无论方法正常返回还是抛出异常,监控信息都能被记录;System.currentTimeMillis()前后差值即为方法执行耗时。

仓库 Byte-buddy 篇二 中对queryUserInfo方法的监控输出与本文结构一致,验证了这一套注解采集方案的可行性:

方法名称:queryUserInfo 入参个数:2 入参类型:java.lang.String、java.lang.String 出参类型:java.lang.String 出参结果:德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈! 方法耗时:490ms

而在仓库 第8节:在插件中引入探针,基于字节码插桩获取执行SQL 中,同样的思路还被用于拦截com.mysql.jdbc.PreparedStatement#executeInternal,通过@This Object obj拿到当前执行对象后反射读取originalSql字段,从而打印出可直接复制执行的替换 SQL——这充分说明"IDEA 插件 + 字节码探针"是一套可以复用到多种研发提效场景的通用骨架。

五、IDEA 插件侧:把探针挂载到「启动运行」

IDEA 插件开发的知识内容较多,此处演示案例的插件开发部分相对简单,核心是在程序启动时添加我们的字节码插桩程序

1. 方案一:继承 DefaultJavaProgramRunner 重写 doExecute(本文主案例)

主要方式是继承com.intellij.execution.impl.DefaultJavaProgramRunner,重写doExecute方法,添加自己需要的内容:

@Override protected RunContentDescriptor doExecute(@NotNull RunProfileState state, @NotNull ExecutionEnvironment env) throws ExecutionException { JavaParameters parameters = ((JavaCommandLine) state).getJavaParameters(); // 信息获取 PsiFile psiFile = env.getDataContext().getData(LangDataKeys.PSI_FILE); String packageName = ((PsiJavaFileImpl) psiFile).getPackageName(); // 添加字节码插装 ParametersList parametersList = parameters.getVMParametersList(); parametersList.add("-javaagent:" + this.getClass().getResource("/").getPath().substring(1) + "ProjectProbe.jar=" + packageName); return super.doExecute(state, env); }

这段代码的核心动作拆解:

  • ((JavaCommandLine) state).getJavaParameters():拿到本次运行配置的 JVM 启动参数对象;
  • env.getDataContext().getData(LangDataKeys.PSI_FILE):从运行环境的数据上下文中取到当前编辑的 PSI 文件;((PsiJavaFileImpl) psiFile).getPackageName()得到当前源码的包名——这个包名会作为探针的agentArgs传入,用于限定插桩范围;
  • parameters.getVMParametersList().add("-javaagent:...ProjectProbe.jar=" + packageName):把-javaagent参数拼接到本次运行进程的 VM 参数列表中。其中this.getClass().getResource("/").getPath().substring(1)用于定位插件类路径下探针 Jar(ProjectProbe.jar)的所在目录,=packageName即把包名作为 agent 参数传给premain
  • 最后调用super.doExecute(state, env)继续走原生运行流程,对研发的启动运行习惯零侵入

此处最核心的就是-javaagentProjectProbe.jar工程探针程序的 Jar 包加载进去,其余是一些PsiFileAPI 的使用。

2. 方案二:JavaProgramPatcher + plugin.xml 声明(仓库第8节同构实现)

仓库 第8节:在插件中引入探针 提供了同一套能力在工程结构上的另一种组织方式,工程拆分为两个模块:

guide-idea-plugin-probe ├── probe-agent # 探针模块:编译打包字节码增强服务,产出 Jar └── probe-plugin # 插件模块:通过 java.programPatcher 加载字节码增强包

探针侧在 gradle 打包时需引入shadowJarPremain-Class打包进可执行 Jar;插件侧通过build.gradle声明依赖本地 Jar 文件树:

dependencies { implementation fileTree(dir: 'libs', includes: ['*jar']) }

然后编写补丁类继承com.intellij.execution.JavaProgramPatcher,重写patchJavaParameters向 VM 参数追加-javaagent

public class PerRun extends JavaProgramPatcher { @Override public void patchJavaParameters(Executor executor, RunProfile configuration, JavaParameters javaParameters) { RunConfiguration runConfiguration = (RunConfiguration) configuration; ParametersList vmParametersList = javaParameters.getVMParametersList(); vmParametersList.addParametersString("-javaagent:" + agentCoreJarPath); vmParametersList.addNotEmptyProperty("guide-idea-plugin-probe.projectId", runConfiguration.getProject().getLocationHash()); } }

最后在plugin.xml中把补丁类注册到java.programPatcher扩展点:

<extensions defaultExtensionNs="com.intellij"> <!-- Add your extensions here --> <java.programPatcher implementation="cn.bugstack.guide.idea.plugin.PerRun"/> </extensions>

这样插件安装后,每次通过 IDEA 运行代码,都会自动执行到拦截采集逻辑。对比两种方案可以看出:DefaultJavaProgramRunner.doExecute重写更贴近"运行时自定义"(本文主案例采用的方式),而JavaProgramPatcher则把参数补丁声明式地挂到扩展点上,两种方式都能达到"嵌入启动运行、自动注入探针"的目标。

3. 从控制台到服务端:数据的传输与处理

在本文的演示阶段,采集到的接口信息直接输出到控制台。而落到真实生产环境,还需要第三步:数据的传输和处理

  • 传输:可以使用 MQ 或者直接用 Netty 把研发运行过程中采集到的方法信息异步上报;
  • 处理:服务端对上报的数据进行分析,识别有价值的数据、把同类数据合并为一条执行链路的数据、按需求代码分支聚合全部接口动作、生成各系统间的关系链路,并配套生成最新的接口文档与一键测试验证功能;
  • 呈现:测试人员介入时可以参考研发编码过程中的全部测试用例、查看功能覆盖程度,测试过程中的数据也会被保留,最终生成一套完整的"研发测试质量交付全览图"。

六、效果演示

安装插件:安装方式与正常安装 IDEA 插件一致。由于该插件处于开发阶段,需要通过本地安装(Install Plugin from Disk...)方式加载构建产物。

运行效果:在安装了插件的 IDEA 中运行任意含接口/方法的测试代码,运行过程中接口的信息会被完整输出到控制台,包括方法名称、入参个数与内容、出参类型与结果、方法耗时等。在实际使用中,这部分信息会传回服务端,由服务端分析处理后展示在页面上,形成接口文档与工程服务地图。

说明:仓库中该案例为演示性质,运行效果以控制台输出为准;生产化的服务端采集与展示可参考「数据传输与处理」小节自行扩展。

七、总结

  • 基于 IDEA 插件和字节码插桩技术,能做的功能实现还有很多。本文仅仅演示了其中一种"研发到测试痛点"的解决方案:把代码质量评估从提测后置前移到编码过程中,让接口信息、出入参、异常、调用链在开发运行的那一刻就被采集沉淀。
  • 这一方案的技术栈可以完整复用到其他场景:仓库 第8节:在插件中引入探针,基于字节码插桩获取执行SQL 展示了拦截 JDBC 执行 SQL 的变体;基于JavaAgent的全链路监控系列 展示了线程耗时、链路追踪、JVM 与 GC 信息等更多采集维度。
  • 当你看到这样的案例以后,希望能给你的是:并不一定所有的技术点都是为了面试"造火箭"对答的。当你真的把它落地以后,才会懂得自己需要很多知识——从字节码增强到 IDE 扩展点,从进程内采集到 MQ/Netty 传输,再到服务端的数据聚合与可视化,每一个环节都是真实工程能力的锤炼。

延伸阅读(仓库内路径)

  • 《IntelliJ IDEA 插件开发》第8节:在插件中引入探针,基于字节码插桩获取执行SQL
  • 中间件系列 第 18 章:基于IDEA插件开发和字节码插桩技术,采集研发过程中代码执行信息
  • 字节码编程,Byte-buddy 篇一《基于Byte Buddy语法创建的第一个HelloWorld》
  • 字节码编程,Byte-buddy 篇二《监控方法执行耗时动态获取出入参类型和值》
  • 基于JavaAgent的全链路监控一《嗨!JavaAgent》
  • 基于JavaAgent的全链路监控二《通过字节码增加监控执行耗时》

【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 10:15:15

SWAT模型运行报错排查指南:从TxtInOut到成功运行

1. 从TxtInOut文件夹说起&#xff1a;SWAT跑起来之前的那道坎很多人以为SWAT模型最难的环节在数据准备和参数率定&#xff0c;但真正让新手卡住动弹不得的&#xff0c;往往是点击"Run SWAT"之后那几秒——要么弹出一个看不懂的报错框&#xff0c;要么运行进度条走到一…

作者头像 李华
网站建设 2026/9/23 10:13:07

Apache Pulsar IO 连接器全解析:Source、Sink 与处理保证机制实战指南

Apache Pulsar IO 连接器全解析&#xff1a;Source、Sink 与处理保证机制实战指南 【免费下载链接】pulsar Apache Pulsar - distributed pub-sub messaging system 项目地址: https://gitcode.com/gh_mirrors/pulsar28/pulsar 消息系统只有在能够轻松与数据库、其他消息…

作者头像 李华
网站建设 2026/9/23 10:07:37

阿里云ROS Terraform托管服务:企业级IaC运行时底座

1. ROS Terraform 托管服务不是“ROS机器人操作系统”的缩写&#xff0c;而是阿里云资源编排服务&#xff08;Resource Orchestration Service&#xff09;的官方命名刚看到标题里“ROS Terraform”这个组合&#xff0c;很多刚接触云基础设施的同学第一反应是&#xff1a;“ROS…

作者头像 李华
网站建设 2026/9/23 10:06:37

存储过程——游标:从 OPEN-FOR 到 FOR 循环的 TaoToken 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/23 10:05:47

Python代码质量检查工具Flake8详解与应用指南

1. 为什么我们需要代码检查工具在编写Python代码时&#xff0c;即使是最有经验的开发者也会不经意间引入各种问题。从简单的空格错误到潜在的逻辑缺陷&#xff0c;这些"小问题"往往会像滚雪球一样&#xff0c;最终导致难以调试的错误。这就是为什么我们需要像Flake8这…

作者头像 李华