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注解采集方法出入参与耗时、如何通过继承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、JavassistByte 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对返回原始类型的方法注入0FieldValue注入被拦截对象的一个字段的值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.MFManifest-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)用于定位插件类路径下探针 JarProjectProbe.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 参数追加-javaagentpublic 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 defaultExtensionNscom.intellij !-- Add your extensions here -- java.programPatcher implementationcn.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),仅供参考