四、JaCoCo插件配置(Gradle):在build.gradle中配置JaCoCo插件,自定义任务
咱们直接进入实战。Gradle 项目里配置 JaCoCo,说白了就两步:引入插件,再搞定你自己的任务。我记得刚接手一个遗留项目时,团队的构建脚本乱得像毛线团,跑覆盖率报告要手动敲命令——嗯,后来我把这一套标准化了,大家只要跑一个任务就能拿到增量报告。
4.1 标准插件 vs 自定义任务
先说说标准做法。JaCoCo 有官方 Gradle 插件,你只要在 build.gradle 里加上:
plugins {
id 'java'
id 'jacoco'
}
jacoco {
toolVersion = "0.8.11"
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
html.required = true
csv.required = false
}
}
这样就能用 ./gradlew jacocoTestReport 生成覆盖率报告了。简单吧?但我在项目中遇到过一个问题:标准任务只跑全量覆盖率,你没法单独控制“哪些类要测”、“哪些类跳过”。尤其是做大项目时,全量跑一次要十几分钟,谁等得起?
4.2 自定义 JaCoCo 任务的核心套路
自定义任务的原理其实很直白——你手动创建一个 JacocoReport 类型的任务,然后把你要分析的类目录、源码目录、执行数据文件喂给它。你想分析哪个模块、哪个包,都由你说了算。
关键点:自定义任务依赖 .exec 执行数据,这个数据来自测试运行。所以你得先跑测试,再生成报告。
来看看我常用的模板:
task incrementalCoverageReport(type: JacocoReport) {
// 依赖所有测试任务
dependsOn test
// 只分析 src/main/java 下的文件(排除测试代码)
sourceDirectories.setFrom(files("$projectDir/src/main/java"))
// 只检查特定的包:com.example.changed
classDirectories.setFrom(
fileTree(dir: "$buildDir/classes/java/main",
include: ['com/example/changed/**'])
)
// 指定 .exec 文件(测试执行后的覆盖率数据)
executionData.setFrom(file("$buildDir/jacoco/test.exec"))
// 输出格式:HTML + XML
reports {
xml.required = true
html.required = true
html.outputLocation = layout.buildDirectory.dir("reports/jacoco/incremental")
}
}
这里我做了两件事:
- 通过
classDirectories限定只分析com.example.changed包下的类。 - 把输出目录单独指定,避免和全量报告混在一起。
我的经验:一开始我全量跑,后来发现每天改动的文件就那么几个。自定义任务配上 Git diff 来动态筛选,时间从 5 分钟降到 30 秒。你想想,效率提升 10 倍,队友都跑来问怎么做到的。
4.3 增量覆盖率任务的典型配置
下面给出一套完整可用的配置,它动态获取 Git 仓库中本次变更的 Java 文件,然后只对这些文件生成覆盖率报告。
import org.ajoberstar.grgit.Grgit
def getChangedClasses() {
def repo = Grgit.open(currentDir: project.rootDir)
def diff = repo.diff() // 与最新提交对比
// 只保留 .java 文件,并转为类路径格式
return diff.files
.findAll { f -> f.newPath?.endsWith('.java') }
.collect { f ->
f.newPath
.replace('src/main/java/', '')
.replace('.java', '')
.replace('/', '.')
}
}
task incrementalReport(type: JacocoReport, group: 'Coverage') {
dependsOn test
def changed = getChangedClasses()
if (changed.isEmpty()) {
logger.warn('没有变更的 Java 文件,跳过覆盖率报告')
return
}
sourceDirectories.setFrom(files("src/main/java"))
classDirectories.setFrom(
fileTree(dir: "$buildDir/classes/java/main",
include: changed.collect { it.replace('.', '/') + '.class' })
)
executionData.setFrom(file("$buildDir/jacoco/test.exec"))
reports {
xml.required = true
html.required = true
html.outputLocation = file("$buildDir/reports/incremental")
}
}
这段代码依赖 Grgit 库,需要在 build.gradle 顶部引入:
buildscript {
repositories { mavenCentral() }
dependencies { classpath 'org.ajoberstar:grgit:5.2.2' }
}
为什么要用 Grgit?因为我直接用 git diff 命令解析起来太蛋疼,而且跨平台容易出 bug。Gradle 生态里 Grgit 是最成熟的 Git 操作库,你不妨试一试。
避坑指南:我曾经在 CI 环境里忘记配置 Grgit.open() 的当前目录,导致总是拿不到变更文件。务必在 currentDir: project.rootDir 明确指定路径。另外,CI 上如果只拉取了浅层 clone(--depth=1),diff 会失败。解决办法是用 git fetch --unshallow 或设置 fetch-depth: 0。
4.4 多模块项目的配置方式
如果你的项目是微服务或者多模块结构,需要在根项目和子模块间传递 .exec 文件。看下面的示意图就明白了:
上面这张流程图画出了每个子模块运行自己的测试,产生 .exec 文件。根项目里写一个 JacocoReport 任务,配置 executionData 指向所有子模块的输出:
task aggregatedIncrementalReport(type: JacocoReport) {
dependsOn subprojects.test
// 收集所有子模块的 .exec 文件
executionData = files(
subprojects.collect { it.file("$buildDir/jacoco/test.exec") }
)
// 包含所有子模块的源码目录
sourceDirectories = files(
subprojects.collect { it.file("src/main/java") }
)
// 从每个子模块的 classes 目录中筛选变更类
classDirectories = files(
subprojects.collect { project ->
fileTree(dir: project.file("$buildDir/classes/java/main"),
include: getChangedClasses(project))
}
)
reports {
html.required = true
html.outputLocation = file("$buildDir/reports/aggregated-incremental")
}
}
这里要注意,getChangedClasses(project) 需要针对每个子模块单独计算 Git diff,因为不同模块的变更文件路径前缀不同。我一般把它拆成一个独立函数,传入 project 对象。
4.5 自定义任务的常见参数表
| 参数 | 类型 | 说明 | 我常用的推荐值 |
|---|---|---|---|
sourceDirectories | FileCollection | Java 源码目录,用于报告里显示代码行 | files("src/main/java") |
classDirectories | FileTree | 编译后的 .class 文件目录,可设 include/exclude | 根据变更类动态生成 include 列表 |
executionData | FileCollection | .exec 或 .ec 文件,测试运行时生成的覆盖率数据 | file("$buildDir/jacoco/test.exec") |
reports.html.outputLocation | Directory | HTML 报告输出目录 | layout.buildDirectory.dir("reports/jacoco/incremental") |
reports.xml.required | boolean | 是否生成 XML 报告 | true(方便 CI 解析) |
4.6 几个实用小技巧
- 跳过无变更的模块:在子模块的 tasks 里加点条件,比如
onlyIf { !getChangedClasses().isEmpty() },避免每次跑一堆空任务。 - 一次性拿到所有 .exec:我喜欢在根项目定义
task collectExec(type: Copy) { from subprojects.collect { it.file("$buildDir/jacoco") }; into "$buildDir/allExec" },然后自定义报告依赖这个复制的任务。 - 区分本地和 CI:本地跑增量,CI 跑全量。用
project.hasProperty('incremental')来开关,./gradlew check -Pincremental就触发增量模式。
说到底,自定义 JaCoCo 任务的核心能力就是让你精确控制“测什么、看什么”。配合 Git diff 后,每次提交反馈速度飞起。我个人一直在用这种方式,开发效率和覆盖率质量两头都抓住。