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注解采集方法出入参与耗时、如何通过继承DefaultJavaProgramRunner或JavaProgramPatcher让 IDEA 插件在"启动运行"时自动注入探针,最终把代码质量评估从"提测后置"提前到"开发过程中"。
一、背景:研发交付质量为什么要"前置评估"
业务提需求、产品定方案、研发做实现、测试验流程,四个角色的相互配合是确保一个需求上线的必备条件。在整个需求的交付质量级别划分中,研发与测试是非常重的一环——如果研发提测的代码质量不高,就会出现不同级别的修 BUG、返工甚至重做的风险。
那么,怎么来提高代码质量呢?一般我们都会要求研发在开发代码的过程中编写单元测试,验证自己的代码逻辑。如果最终单元测试覆盖度不足,可以由测试拒绝研发提测。
但是,整个需求实现的代码是在全部开发完成后提测的,也就是临近上线的最后一环,大家才知道某个研发的某个功能域的实现是否具备提测条件。如果这个时候代码质量不高,那么接下来就是项目风险的时候——压测试时间、调上线时间,总之"有病拖着最后成大病了"。
当然,你可以在项目开发期间定期排查代码,或者通过日会进度反馈等手段。但这样需要耗费大量时间的 1 对 1 人工排查方式,很难满足复杂流程的较大型项目开发,而且对项目风险把控也是不可预估的。
所以,我们希望采集研发在开发过程中的执行动作,把风险判断提前。实际操作举例就是:当你开发完成一个接口、开始测试运行时,插件就可以采集到这个接口的全部信息,包括接口名称、入参类型和内容、出参类型和内容、异常信息、调用关系链等。这些信息汇总提交到服务端后,可以生成本次需求代码分支下的全部接口动作,以及各系统间的关系链路,并附带随时生成最新的接口文档和一键测试验证功能。后期测试人员介入时就可以参考研发在编码过程中的全部测试用例,也可以查看整个功能的覆盖程度,测试人员测试过程中的数据同样会被保留。有了这些数据,就能完整生成一套"研发测试质量交付全览图",让整个工程开发交付质量评估透明化。
这就是本文要实践的目标:把代码质量检查从"提测后"前移到"编码中"。
二、需求目的与总体技术选型
要实现上述目标,需要三项核心技术配合:
- 字节码插桩:因为要采集到接口执行信息,就需要使用字节码插桩组件给接口方法增强。这个实现有点类似谷歌 Dapper 大规模分布式架构的非入侵监控,只不过这里需要采集的描述性信息更多。关于字节码插桩,可以了解 ASM、Javassist、Byte-Buddy,它们都可以做此项工作。
- IDEA 插件开发:要在研发人员开发过程中进行采集、同时不破坏研发的操作习惯,最好的方式就是嵌入到「启动运行」中——只要在开发过程中有运行代码的动作,就采集相应的接口信息。
- 数据传输与处理:传输可以使用 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 监控一个业务方法只需要三步:
- 通过
ByteBuddy().subclass(...)+ElementMatchers.named("方法名")定位目标方法; - 通过
MethodDelegation.to(MonitorDemo.class)把方法执行委托给监控类; - 在监控类中使用
@RuntimeType、@Origin、@SuperCall等注解获取方法、调用链与耗时信息。
也就是说,Byte Buddy 的使用方式"有些像使用 AOP 的拦截方式",把最核心的字节码增强逻辑封装成高级 API,让开发者把精力放在"采集什么信息"而非"如何改写字节码"上。
常用注解速查表
在编写监控方法时,Byte Buddy 提供了一系列注解用于绑定不同的上下文信息。下表整理自仓库 Byte-buddy 篇二文档:
| 注解 | 说明 |
|---|---|
@Argument | 绑定单个参数 |
@AllArguments | 绑定所有参数的数组 |
@This | 当前被拦截的、动态生成的那个对象 |
@Super | 当前被拦截的、动态生成的那个对象的父类对象 |
@Origin | 可绑定到Method(被调用的原始方法)、Constructor、Class、MethodHandle、MethodType、String(动态类的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)继续走原生运行流程,对研发的启动运行习惯零侵入。
此处最核心的就是-javaagent把ProjectProbe.jar工程探针程序的 Jar 包加载进去,其余是一些PsiFileAPI 的使用。
2. 方案二:JavaProgramPatcher + plugin.xml 声明(仓库第8节同构实现)
仓库 第8节:在插件中引入探针 提供了同一套能力在工程结构上的另一种组织方式,工程拆分为两个模块:
guide-idea-plugin-probe ├── probe-agent # 探针模块:编译打包字节码增强服务,产出 Jar └── probe-plugin # 插件模块:通过 java.programPatcher 加载字节码增强包探针侧在 gradle 打包时需引入shadowJar把Premain-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),仅供参考