大数跨境
0
0

用代码染色实现精准无效代码清理

用代码染色实现精准无效代码清理 阿里云开发者
2025-12-22
4
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参数,实现对生产环境的选择性采样。

二、落地方案

整体流程如下:
流程包括:
  1. 代码插桩:通过Agent在线插入探针
  2. 覆盖采集:请求执行渗透至每行代码,完成染色
  3. 周期dump:定期将内存中的执行数据导出至OSS
  4. 覆盖率计算:结合.class文件生成XML报告
  5. 插件加载:IDEA打开项目时自动拉取最新报告
  6. 代码治理:根据报告清理或重构代码

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) 具体步骤:
  1. 从OSS下载最新.exec采集数据
  2. 通过Git拉取master分支代码并编译生成.class文件
  3. 调用JaCoCo report功能生成XML报告
  4. 上传报告至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应用,但技术路径具备通用性,尤其适用于历史包袱重、重构难度大的系统。未来将持续优化治理体系,推动更多业务接入。
【声明】内容源于网络
0
0
阿里云开发者
阿里巴巴官方技术号,关于阿里的技术创新均呈现于此。
内容 3595
粉丝 0
阿里云开发者 阿里巴巴官方技术号,关于阿里的技术创新均呈现于此。
总阅读23.4k
粉丝0
内容3.6k