一、代码覆盖率采集
1.1 JVM Agent 概述
动态修改类的字节码(Class File) 获取 JVM 中已加载的所有类 添加类文件转换器(ClassFileTransformer) 重新定义类(redefineClasses) 重置类的字节码(reset class definitions)
使用agent方式是在jvm启动参数指定 -javaagent 来加载agent jar包,agent需要实现AgentMain接口。 使用attach的方式会创建独立JVM,占用独立内存资源,使用attach的方式将插桩jar包作用于目标jvm,插桩jar包需要实现PreMain的接口,代码示例:
String jarFile = args[0];String pid = getPid(args);logger.info("Attaching agent to PID: " + pid);VirtualMachine vm = null;try {vm = VirtualMachine.attach(pid);vm.loadAgent(jarFile);logger.info("Agent attached successfully");} catch (IOException ioException) {logger.critical("load agent jar fail: " + jarFile);} catch (AttachNotSupportedException attachNotSupportedException) {logger.critical("attach to jvm fail: " + pid);} catch (AgentLoadException | AgentInitializationException agentException) {logger.critical("jvm load agent or agent init fail: " + pid);} finally {if (vm!=null) {vm.detach();}}
1.2 代码执行覆盖
执行效率低,尤其是按行插桩,侵入性太大; 并发情况下存在锁竞争风险,虽然实际结果对统计代码执行覆盖没影响; 执行结果使用上不便于集成IDEA,对代码覆盖的可视化支持难度大;
public void exampleMethod(int a, int b) {// do some thing 代码块0if(a > 0){// do some thing 代码块1}else if(b > 0){// do some thing 代码块2}// do some thing 代码块3}
1.3 采集方案对比
<!-- https://mvnrepository.com/artifact/org.jacoco/org.jacoco.agent --><dependency><groupId>org.jacoco</groupId><artifactId>org.jacoco.agent</artifactId><version>0.8.12</version><scope>runtime</scope><classfier>runtime</classfier></dependency>
1.4 方案选择
二、落地方案
2.1 整体设计
代码采集:支持应用代码执行覆盖率采集(主要为.exec文件),不影响原有业务逻辑;
基于agent的方式,持续周期性采集; 支持长期数据文档采集,合并多天数据;
数据合并:代码执行情况的采集数据与最新代码版本生成完整代码覆盖情况;(最终代码覆盖=.exec文件+.class文件)
IDEA插件:代码执行情况可视化,IDEA插件支持目标应用代码执行情况的可视化
IDEA目标项目检测,非目标项目提醒; 目标项目打开自动下载oss覆盖数据,也可配置第一次使用时下载; 支持多天数据采集下载和采集结果合并,提高代码采集覆盖准确性; 打开代码执行覆盖情况,展示package、class执行覆盖率和代码行级执行情况; 隐藏代码执行覆盖情况,关闭package、class执行覆盖率和代码行级执行情况的展示; 支持采集数据的缓存和刷新,无需重复下载oss数据; 右键和工具栏支均支持打开&关闭展示、工具栏和配置页支持插件相关的配置;
2.2 代码采集
private void initClassLoader(ClassLoader parentClassLoader, File appPath, File appJar, String path, String appName) {try {File[] extFile = appPath.listFiles((dir, name1) -> "ext".equals(name1));AppExtClassLoader extClassLoader = null;if (extFile != null && extFile.length > 0) {File[] extJars = extFile[0].listFiles((dir, name1) -> name1.endsWith(".jar"));extClassLoader = new AppExtClassLoader(parentClassLoader, extJars);}File unzipFile = Paths.get(path, appName).toFile();ZipFileUtils.unzip(new ZipFile(appJar), unzipFile);this.classLoader = new AppClassLoader(unzipFile, extClassLoader != null ? extClassLoader : parentClassLoader);} catch (Throwable e) {throw new ContainerException("container constructor error init classloader", e);}}
wget -c -O /home/admin/app/jacoco-runtime.jar "https://repo1.maven.org/maven2/org/JaCoCo/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar" && \/*** 使用jacoco的数据结构做dump,返回dump是否成功** @param filePath dump的位置* @return 是否执行成功*/boolean jacocoDump(String filePath) throws IOException {Agent iAgent = Agent.getInstance();if (iAgent == null) {DosaLogUtil.warnNew("Jacoco agent not found!");return false;}AgentOptions agentOptions = buildOptions(filePath);FileOutput fileOutput = new FileOutput();fileOutput.startup(agentOptions, iAgent.getData());fileOutput.writeExecutionData(true);return true;}
2.3 数据合并
/*** 克隆代码仓库** @param config 应用配置* @return 本地仓库路径,失败返回null*/public String cloneRepository(CodeProfilerAppConfigDO config) {String appName = config.getAppName();String localRepoPath = buildLocalRepoPath(appName);try {String ciToken = kcUtil.decrypt(config.getCiToken());GitCloneRequest cloneRequest = new GitCloneRequest().setRepoUrl(config.getGitUrl()).setBranch(config.getDefaultBranch()).setTargetDir(localRepoPath).setCiToken(ciToken);GitHelper.clone(cloneRequest);return localRepoPath;} catch (Exception exception) {LOGGER.error("cloneRepository:clone failed for app: " + appName, exception);return null;}}
private static final String MAVEN_CMD = "/opt/apache-maven-3.9.11/bin/mvn";private static final String MAVEN_COMPILE = "compile";private static final String MAVEN_SKIP_TESTS = "-DskipTests=true";private static final String MAVEN_TEST_SKIP = "-Dmaven.test.skip=true";private static final String MAVEN_AUTO_CONFIG_INTERACTIVE = "-Dautoconfig.interactive=off";private static final String MAVEN_PROJECT_BUILD_SOURCE_ENCODING = "-Dproject.build.sourceEncoding=UTF-8";/*** Maven编译项目** @param config 应用配置* @param localRepoPath 本地仓库路径* @return 成功返回true,失败返回false*/public boolean compileProject(CodeProfilerAppConfigDO config, String localRepoPath) {String appName = config.getAppName();String[] commands = new String[] {MAVEN_CMD, MAVEN_COMPILE, MAVEN_SKIP_TESTS, MAVEN_TEST_SKIP, MAVEN_AUTO_CONFIG_INTERACTIVE,MAVEN_PROJECT_BUILD_SOURCE_ENCODING};try {int exitCode = MavenHelper.execute(commands, localRepoPath);if (exitCode == 0) {LOGGER.info("maven build succeeded for app: " + appName);return true;} else {LOGGER.error("maven build failed with exit code:" + exitCode);return false;}} catch (IOException | InterruptedException exception) {LOGGER.error("compileProject:maven build failed for app:" + appName, exception);return false;}}
/*** 创建XML格式的覆盖率报告** @param execFileLoader 执行数据加载器* @param bundleCoverage 覆盖率分析结果* @param xmlPath XML报告文件路径* @throws Exception 创建失败*/private void createXmlReport(ExecFileLoader execFileLoader, IBundleCoverage bundleCoverage, String xmlPath)throws Exception {final List<IReportVisitor> visitors = new ArrayList<>();final XMLFormatter formatter = new XMLFormatter();visitors.add(formatter.createVisitor(Files.newOutputStream(Paths.get(xmlPath))));IReportVisitor reportVisitor = new MultiReportVisitor(visitors);reportVisitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),execFileLoader.getExecutionDataStore().getContents());reportVisitor.visitBundle(bundleCoverage, null);reportVisitor.visitEnd();}
2.4 插件设计
打开&关闭代码执行覆盖展示:手动打开或者关闭代码的覆盖率数据展示; 数据自动/手动下载:在覆盖率数据展示时,自动下载oss的数据到本地(如果本地没有缓存数据); 插件配置:支持配置插件相关的参数,例如oss的配置、下载最近N天数据、缓存周期、下载超时等; 数据缓存:支持将下载的数据缓存在本地,只要缓存未过期,优先使用本地数据。
三、治理效果
四、收获与反思
探索了JaCoCo工作原理,学习其优秀的代码设计(主要基于访问者模式,结合asm类代码修改),最终借鉴其框架实现了代码执行覆盖的采集; D应用代码历史悠久,通过对执行数据的分析和代码业务的判断,实现了对D应用无效代码的规模化清理,整体过程对业务无感;
前期没有深入了解热部署类加载原理和版本管理,导致对部署代码的覆盖采集问题出现偏差,误导了问题的定位方向;
对使用AI agent完整开发IDEA插件前期期望过高,利用AI从设计到代码开发做完整插件实现,存在一些问题:
前期设计的方案后续每做一次需求调整,AI的修改都有可能让代码设计更加复杂,这种情况因token限制导致下次对话上下文丢失更加严重; IDEA平台内部的代码实现代码对AI偏黑盒,经常在代码库检索不到,想让AI复用IDEA内部接口功能较难实现; 对IDEA插件领域模型不熟悉的话,agent生成的代码逻辑自己都不一定搞懂原理,更别说后续做问题定位和插件升级。
而最终版插件的实现方案,还是人工通过调试IDEA社区版源码让IDEA的Coverage插件逻辑白盒化,使用原生代码覆盖率的接口实现了插件核心逻辑,而像bug的定位&修复等工作使用AI确实反而高效,算是“锦上添花”了。
采集数据基于安全生产环境,结合业务功能清理代码,基本不会影响线上核心链路,但是采集过程没有完全覆盖线上请求,诸如冷链路、大促链路、老版本逻辑让人防不胜防,清理过程也有遇到清理的类仍有少量流量的情况。
JaCoCo agent:https://www.eclemma.org/jacoco/trunk/doc/agent.html JaCoCo cli:https://www.eclemma.org/jacoco/trunk/doc/cli.html
Action:https://plugins.jetbrains.com/docs/intellij/plugin-actions.html?from=DevkitPluginXmlInspection
projectService:https://plugins.jetbrains.com/docs/intellij/plugin-services.html?from=DevkitPluginXmlInspection applicationConfigurable:https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html?from=DevkitPluginXmlInspection#exploring-available-extensions
IDEA插件开发:https://plugins.jetbrains.com/docs/intellij/plugins-quick-start.html?from=DevkitPluginXmlInspection git仓库:https://github.com/JetBrains/intellij-community/tree/pycharm/233.15619.17?tab=readme-ov-file#readme
主动式智能导购 AI 助手构建
为助力商家全天候自动化满足顾客的购物需求,可通过百炼构建一个 Multi-Agent 架构的大模型应用实现智能导购助手。该系统能够主动询问顾客所需商品的具体参数,一旦收集齐备,便会自动从商品数据库中检索匹配的商品,并精准推荐给顾客。