
D是某团队历史悠久的
服务端应用,其代码库源自淘宝无线端迁移,大量历史代码已无线上流量但未被清理,形成“代码沉淀”,不仅增加新人学习成本,也提升了维护负担。由于人工判断代码是否可下线效率低且易误删,团队引入基于代码执行染色与覆盖率分析的技术方案,实现科学、高效的无效代码治理。 该方案依托JVM Agent机制进行字节码插桩,在运行时采集代码执行情况,并通过IDEA插件将覆盖率数据可视化,辅助开发者精准识别并清理无用代码,显著提升代码健康度和开发效率。
一、代码覆盖率采集
1.1 JVM Agent 概述
Java Instrumentation接口支持在JVM运行时动态修改类字节码,是Java Agent机制的核心,广泛应用于性能监控、AOP、热部署及代码覆盖率分析等场景。Instrumentation由JVM在加载Agent时自动注入,主要功能包括:
- 动态修改类字节码
- 获取已加载类信息
- 添加类文件转换器(ClassFileTransformer)
- 重新定义类(redefineClasses)
- 重置类定义
Agent可通过两种方式加载:
| 类别 |
依赖重启 |
长期采集 |
资源占用 |
| agent |
是 |
稳定 |
共用jvm |
| attach |
否 |
重启失效 |
独立jvm |
使用-agentpath参数启动JVM可加载Agent JAR包,需实现premain方法;Attach方式则通过VirtualMachine API动态附加到目标JVM,适用于临时诊断,但独立进程占用额外内存。
1.2 代码执行覆盖
代码覆盖率通常包括行覆盖、分支覆盖和方法覆盖。本方案聚焦行覆盖率,用于识别长期未被执行的代码路径。
1.2.1 自定义插桩方案
基于ASM和ClassFileTransformer实现按行或按方法插桩,虽灵活性高,但存在以下问题:
- 按行插桩性能开销大,侵入性强
- 并发环境下可能引发锁竞争
- 数据难以集成至IDE,可视化支持弱
1.2.2 JaCoCo方案
JaCoCo通过字节码插桩插入布尔数组探针$jacocoInit[],记录代码块执行状态。每个元素对应一个控制流图(CFG)中的基本块,值为true表示该块被执行。相比逐行计数,此方式性能更高,避免锁竞争。 示例代码经JaCoCo插桩后生成多个代码块,其执行流转由CFG描述:
JaCoCo提供Agent和CLI
工具,支持高效集成。
1.3 采集方案对比
1.3.1 自研插桩 vs JaCoCo工具
|
优点 |
缺点 |
| 自研插桩 |
灵活性高、数据处理自由 |
开发成本高、稳定性需验证 |
| JaCoCo工具 |
稳定高效、快速集成 |
数据格式固定,需二次加工 |
综合考虑稳定性与性能,团队选择JaCoCo作为基础采集框架。
1.3.2 agent vs attach
|
使用方式 |
性能影响 |
重启影响 |
卸载插桩 |
| agent方式 |
jvm启动参数增加agent参数 |
随jvm启动,业务无感,平均cpu会上涨 |
重启不失效 |
重新发布 |
| attach方式 |
独立jvm attach到业务jvm |
attach时触发插桩导致jvm飙升,后续稳定 |
重启失效 |
无需重启 |
agent适合长期采集,attach适合临时分析,参考Arthas模式。
1.3.3 在线 vs 离线
在线插桩在运行时通过Agent完成;离线插桩则在构建阶段通过Maven插件修改字节码。离线方案需在部署时引入JaCoCo runtime依赖,否则会抛出TypeNotPresentException。
<dependency> <groupId>org.jacoco</groupId> <artifactId>org.jacoco.agent</artifactId> <version>0.8.12</version> <scope>runtime</scope> <classifier>runtime</classifier></dependency>
注意必须使用`
runtime
`,以确保包含完整依赖包。 离线方案缺点在于需维护两套部署包(插桩版与非插桩版),增加发布复杂性。
1.4 方案选择
D应用需长期持续采集生产环境代码执行数据,兼顾稳定性与效率。最终确定采用**agent方式 + JaCoCo框架**,通过Docker镜像集成JaCoCo依赖,在启动脚本中配置-javaagent参数,实现对生产环境的选择性采样。
二、落地方案
整体流程如下:
流程包括:
- 代码插桩:通过Agent在线插入探针
- 覆盖采集:请求执行渗透至每行代码,完成染色
- 周期dump:定期将内存中的执行数据导出至OSS
- 覆盖率计算:结合.class文件生成XML报告
- 插件加载:IDEA打开项目时自动拉取最新报告
- 代码治理:根据报告清理或重构代码
2.1 整体设计
系统需具备以下能力:
- 代码采集:支持基于Agent的周期性执行数据采集(.exec文件),不影响业务逻辑
- 数据合并:将采集数据与最新.class文件结合,生成完整覆盖率报告(.exec + .class → XML)
- IDEA插件:实现代码执行情况可视化,支持自动下载、缓存刷新、多日数据合并等功能
2.2 代码采集
2.2.1 热部署的影响
D应用作为容器承载R、B两个热部署子应用,三者通过自定义ClassLoader隔离代码版本。实验证明,JaCoCo Agent可正确对D、R、B所有类完成插桩,不受热部署架构影响。 反编译验证:
2.2.2 代码改造
主要改动如下:
Step1: 下载JaCoCo runtime JAR至镜像指定路径
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" && \
Step2: 配置JVM启动参数,启用Agent并设置白名单
Step3: 实现定时数据dump
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;
}
数据每日凌晨dump一次并上传OSS归档,确保分析时效性。
2.3 数据合并
JaCoCo采集的.exec文件仅含执行标记数组,需结合.class文件才能生成完整覆盖率报告。 公式: 详细执行覆盖率(.xml) = 插桩执行文件(.exec) + 原始编译文件(.class) 具体步骤:
- 从OSS下载最新.exec采集数据
- 通过Git拉取master分支代码并编译生成.class文件
- 调用JaCoCo report功能生成XML报告
- 上传报告至OSS并清理本地缓存
2.3.1 jGit代码克隆
使用jGit客户端配合仓库Token克隆代码,避免使用个人账号,保障安全。
2.3.2 本地Maven编译
在基础镜像中预装Maven,通过ProcessBuilder调用mvn compile命令编译代码。
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";
...
public boolean compileProject(CodeProfilerAppConfigDO config, String localRepoPath) {
String[] commands = {MAVEN_CMD, MAVEN_COMPILE, MAVEN_SKIP_TESTS, ...};
int exitCode = MavenHelper.execute(commands, localRepoPath);
return exitCode == 0;
}
多模块项目需将各module的target/classes合并至统一目录。
2.3.3 覆盖率报告生成
利用JaCoCo的XMLFormatter生成结构化报告。
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(...);
reportVisitor.visitBundle(bundleCoverage, null);
reportVisitor.visitEnd();
}
报告按应用和日期命名,存储于OSS。
2.4 插件设计
2.4.1 插件功能
IDEA插件核心功能包括:
- 手动/自动开关代码覆盖率展示
- 支持OSS数据自动下载与本地缓存
- 可配置缓存周期、超时时间、分析天数等参数
- 支持多日数据合并,提升准确性
2.4.2 插件实现
基于IntelliJ Platform扩展点开发:
- Action:处理用户交互行为(如显示/隐藏覆盖率)
- ProjectService:封装核心业务逻辑
- ApplicationConfigurable:提供配置面板
注册方式:
2.4.3 插件效果
右键菜单提供【显示覆盖率】【隐藏覆盖率】【刷新】等功能。
功能特点:
- 项目视图展示package级覆盖率,便于定位低覆盖路径
- 编辑器左侧标注行级执行状态:绿色(已执行)、红色(未执行)、黄色(部分覆盖)
- Coverage面板展示类、方法、行、分支覆盖率详情
- 顶部Tools菜单支持刷新、配置管理等操作
配置面板支持OSS路径、缓存策略等设置:
三、治理效果
借助执行数据与可视化工具,团队高效推进代码清理工作:
| 应用 |
代码基线 |
清理代码 |
降低比例 |
| B |
21.5w |
15.4w |
71% |
| R |
43.1w |
18.7w |
43% |
| D |
25.2w |
2.1w |
8.3% |
B应用因与R存在大量冗余,清理成效最显著;R应用仍处于高频迭代期,尚有优化空间;D作为底层依赖,清理需更谨慎,正在进行中。
四、收获与反思
主要收获:
- 深入理解JaCoCo原理,借鉴其基于ASM与访问者模式的设计实现高效采集
- 完成D应用规模化无效代码清理,全过程对业务无感知
- 掌握IntelliJ插件开发框架,通过调试社区版源码实现核心功能白盒化
经验教训:
- 初期忽视热部署类加载机制,导致覆盖率采集偏差
- 过度依赖AI生成完整插件代码,因上下文丢失、平台黑盒等问题导致维护困难
- 生产环境采集无法完全覆盖冷门链路(如大促、老版本逻辑),偶现误删风险
当前方案虽仅服务于D应用,但技术路径具备通用性,尤其适用于历史包袱重、重构难度大的系统。未来将持续优化治理体系,推动更多业务接入。