做 Java 后端这些年,每次项目从单模块往多模块迁移,最容易被忽略的不是业务代码本身,而是覆盖率统计这一整套东西。单模块下 JaCoCo 跑得好好的,mvn test结束自动生成一份漂亮的 HTML 报告,看起来一切都很正常。一旦拆成父 POM + 多个子模块的结构,再按原来的方式配置一遍,问题就接踵而来:聚合报告空白、只有某个模块的数据、点进类看不到源码、CI 上报告和本地长得不一样。这篇文章我把踩过的 5 类坑完整复盘一遍,每一条都包含现象、排查链路和最终落地的配置,适合正在做多模块改造、或者准备搭建覆盖率门禁的团队直接对照排查。
1. 从单模块到多模块,聚合报告到底在聚合什么
1.1 JaCoCo 报告生成的三个必需品
聊坑之前,先把 JaCoCo 的工作链条理清楚。一份 JaCoCo 报告要正常生成,必须凑齐三样东西:exec文件、class 文件和 source 文件。
exec文件是测试运行时的证据。prepare-agent目标会在 Maven 的测试 JVM 启动参数里塞一段-javaagent,这段 agent 会记录每一行指令、每一个分支是否被执行,JVM 停止时把结果写到jacoco.exec。class 文件是被测代码的编译产物,JaCoCo 需要拿它做字节码分析,最终算出指令覆盖率、分支覆盖率、圈复杂度这些指标。source 文件则纯粹是为了报告展示,HTML 报告里那个能点击查看源码的功能,靠的就是它。
这三样东西的关系,有点像做工程验收:exec是施工日志,class 是实际结构,source 是设计图纸。单模块项目里,这三样天然放在同一个目录下,prepare-agent生成的exec在当前模块的target,class 也在当前模块的target/classes,source 就在src/main/java。所以单模块几乎不需要任何额外配置,装好插件就能出报告。
1.2 单模块能跑通,多模块为什么就崩
多模块的难点在于,聚合报告需要把 N 个模块的三样东西汇集到同一份报告里。这时候就牵涉到几个单模块时代根本不会考虑的问题:
- 每个模块的
exec文件在哪生成,什么时候生成? - 聚合目标是从哪个模块去扫描其它模块的数据?
- 模块之间的依赖关系、构建顺序会不会影响数据读取?
说实话,很多人踩坑并不是因为 JaCoCo 有多复杂,而是因为多模块构建里,Maven 的 reactor 机制、插件配置继承、属性解析顺序互相交织,任何一个环节没对齐,结果都会静默出错——注意是静默,JaCoCo 很少会直接报 fatal error,它更倾向于"生成一份看起来正常但数据不完整"的报告。这种问题最磨人,因为日志不会告诉你答案。
1.3 后面五个坑的排查路径总览
我把多模块报告聚合最常见的五个问题按根因分了类,方便你先有个全局认识:
| 坑点 | 核心症状 | 根因类别 |
|---|---|---|
| 坑点一 | 聚合报告空白或 0%,测试明明跑了 | argLine参数被覆盖,agent 没进入测试 JVM |
| 坑点二 | 报告只有单模块数据,或输出目录不对 | report和report-aggregate目标混用 |
| 坑点三 | 报告缺了某几个模块,无人报错 | 聚合模块没有声明对目标模块的依赖 |
| 坑点四 | 覆盖率数字有,源码点开一片红 | class/source 路径解析失效 |
| 坑点五 | 换台机器、换个流水线结果就变 | 插件版本不统一、构建时序失控 |
下面逐一拆解。
2. 坑点一:argLine 被上层配置覆盖,exec 文件直接缺失
2.1 现象与复现路径
这个坑我最早是在一个子模块数量超过 20 的工程里碰到的。当时 CI 上聚合报告突然全部显示 0%,本地怎么跑都正常。检查了相当久,最后发现是基础父 POM 里加了一段 JVM 参数配置,把argLine属性整个覆盖了。
这里要先解释一个机制:prepare-agent不会直接修改 JVM,它只是往 Maven 的一个属性里写入 agent 参数,默认这个属性名就是argLine。真正启动测试 JVM 的是maven-surefire-plugin,它读取argLine属性作为测试 JVM 的启动参数。所以完整的链路是:prepare-agent设置argLine属性 → surefire 读取argLine→ 测试 JVM 带上 agent → 测试跑完生成exec。
问题就出在"设置"和"读取"之间。任何一方对argLine做了重复定义,都可能让 agent 参数被冲掉。最常见的两种:
- 父 POM 的
<properties>里定义了<argLine>-Xmx1024m</argLine>,这个值会把prepare-agent写入的值覆盖掉。 - 某个模块的
maven-surefire-plugin配置里直接写了<argLine>-Xmx1024m</argLine>,且没有带上@{argLine}占位符。
结局都一样:测试 JVM 里根本没有 JaCoCo agent,测试照样跑,但target/jacoco.exec永远不会生成。聚合报告读不到exec,自然只能给你一张全白或全 0 的图。
2.2 排查链路:从文件缺失到参数覆盖
如果你也遇到"报告空白但测试正常"的情况,按这个顺序查:
- 在项目根目录执行
find . -name "*.exec" -exec ls -l {} \;,看看哪些模块生成了exec,哪些没有。如果几乎所有模块都没有,基本可以锁定是 agent 没有注入。 - 找一个没生成
exec的模块,执行mvn help:evaluate -Dexpression=argLine -q -DforceStdout,看当前模块解析出来的argLine是什么。如果输出里没有javaagent相关路径,说明这个属性确实被业务配置占了。 - 打开这个模块的
mvn help:effective-pom,重点看maven-surefire-plugin的argLine配置和父 POM 的<properties>。 - 顺手看一眼
target/surefire-reports目录的生成时间,排除"测试压根没跑到"的可能性。
这里有个小技巧,第 2 步的help:evaluate必须在具体模块目录下执行,因为多模块每个子模块可能从不同的父 POM 继承了不同属性。在根目录执行只能看到根 POM 的解析结果,可能会误导你。
2.3 标准解法与验证方式
解法不唯一,但思路是一致的:保证prepare-agent写入的 agent 参数能原封不动地传给 surefire。
推荐做法是,在父 POM 里统一管理 JaCoCo 插件版本:
<properties> <jacoco.version>0.8.12</jacoco.version> </properties> <build> <pluginManagement> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>${jacoco.version}</version> </plugin> </plugins> </pluginManagement> </build>然后在确实需要跑测试的子模块(或者直接在父 POM 的<build>里,让所有子模块继承)加入:
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>default-prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> </executions> </plugin>同时,任何自定义 JVM 参数都不要直接放在<properties>里,而是放到 surefire 的argLine中,并保留@{argLine}占位符:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> <configuration> <argLine>@{argLine} -Xmx1024m</argLine> </configuration> </plugin>@{argLine}是 Maven 3.2.1 之后的延迟解析语法。它会在 surefire 真正执行插件配置时才去解析属性值,这时候prepare-agent已经运行完,argLine里带着完整的 agent 参数,所以最终拼接出来的就是"agent 参数 + 你的业务参数"。
验证是否修复很简单:重新执行mvn clean test,再次用find检查各模块target下是否生成了jacoco.exec。每个模块的exec文件大小应该在几十 KB 到几 MB 之间,如果发现某个模块的exec只有几百字节,基本可以断定这个模块没有正常采集数据。
注意:排查这类问题时,别一上来就怀疑 JaCoCo 版本。exec 文件格式的兼容性整体是好的,大部分"数据互不认账"的案例,真正的原因都是 agent 没注入或者配置被覆盖。
3. 坑点二:report 与 report-aggregate 目标选错,报告形态南辕北辙
3.1 两个 goal 的工作边界差异
很多博客只教你"在 POM 里配 jacoco-maven-plugin,然后执行mvn test就会出报告",不会告诉你这个配置下默认触发的是report目标。report和report-aggregate虽然都是生成覆盖率报告,但工作边界完全不同。
| 目标 | 处理范围 | 默认输出目录 | 适用场景 |
|---|---|---|---|
report | 当前 Maven 项目自身的数据 | target/site/jacoco | 单模块项目 |
report-aggregate | 当前项目及其依赖的本地模块 | target/site/jacoco-aggregate | 多模块聚合 |
report只关心当前 Maven 项目上下文里的exec、class、source。如果当前项目是一个packaging=pom的父模块,它既没有测试,也没有 class 文件,report执行时只会输出一行类似Skipping JaCoCo execution due to missing classes directory的日志,然后什么都不生成。这段日志非常具有迷惑性,因为它不报错,只会在 verbose 日志里出现一行。
report-aggregate则会去扫描当前项目声明的本地依赖,把这些依赖模块的exec、class、source全部找出来,汇总成一份跨模块报告。这才是多模块场景真正需要的目标。
3.2 现象与排查链路
这个坑的典型现象是:在根 POM 配了插件,执行mvn verify,耗时也很正常,看起来"报告生成了",但你去target/site下看,只有jacoco目录,或者根本没有聚合目录。打开 HTML,要么只有父模块本身的数据(通常为空),要么只有某一个模块的数据。
排查的时候,先看输出目录:
target/site/jacoco存在 → 你执行的是report目标。target/site/jacoco-aggregate存在 → 你执行的是report-aggregate目标。
再看构建日志,留意有没有Skipping JaCoCo execution due to missing classes directory这类提示。如果日志里每个模块都打出了Site directory .../target/site/jacoco,说明所有模块都各自执行了report,这时候生成的是一堆分散的单模块报告,不是聚合报告。
最后用mvn help:effective-pom检查当前生效的插件配置,确认<executions>里的<goal>到底是哪个。有时候根 POM 和子模块各配了一版,子模块覆盖了根 POM 的配置,就会出现"根模块是 aggregate,子模块却执行 report"这种混合行为。
3.3 正确的 plugin 配置示例
多模块项目里,标准做法是把聚合配置放在一个专门的聚合模块里,或者直接放在父 POM 供所有模块继承。这里先给一个最直接的版本:
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>aggregate-report</id> <phase>verify</phase> <goals> <goal>report-aggregate</goal> </goals> </execution> </executions> </plugin>如果你把这段配在父 POM 的<build><plugins>里,所有子模块会继承prepare-agent和report-aggregate。每个模块测试时会生成自己的exec,到了verify阶段,每个模块会尝试执行一次report-aggregate,聚合的是它自己依赖的那些模块数据。这会导致聚合报告在每个模块下都生成一份,内容可能略有差异。
如果你想要"只在某一个地方产出一份完整聚合报告",更推荐的做法是新建一个独立的聚合模块,也就是下一个坑要说的核心方案。这里你只需要记住:别再用report目标去做多模块聚合,它的设计就不是干这个的。
4. 坑点三:聚合模块没有声明依赖,report-aggregate 只扫到半张图
4.1 现象:报告缺模块却没人报错
这个坑最坑的地方在于:报告能正常生成,但内容不完整。举个例子,一个工程有mall-common、mall-service、mall-web三个子模块,聚合报告里可能只有mall-web的数据,另外两个模块死活不出现。而且构建日志没有任何红色报错,看着就像mall-web本身的覆盖率。
第一次遇到这个情况时,我第一反应是某个子模块的exec没生成。逐个检查完所有exec都存在后,又怀疑是不是版本问题。最后才发现,根因在聚合模块的依赖声明上。
4.2 原理:report-aggregate 按依赖不按 modules
report-aggregate的目标是"读取当前项目依赖的本地模块数据"。注意关键词是"依赖"。它并不是扫描<modules>列表,而是分析当前 POM 的<dependencies>,然后从这些依赖中挑出属于本地 reactor 的模块,再读取它们的覆盖率数据。
但很多多模块工程的父 POM 是这样的结构:
<groupId>com.example</groupId> <artifactId>mall-parent</artifactId> <packaging>pom</packaging> <modules> <module>mall-common</module> <module>mall-service</module> <module>mall-web</module> </modules>父 POM 只声明了<modules>,没有声明<dependencies>。从 Maven 视角看,父模块和子模块之间是聚合关系(aggregation),不是依赖关系(dependency)。你在父 POM 上执行report-aggregate,它找不到任何"依赖的本地模块",自然只能聚合出一份空报告或残缺报告。
这也是很多教程让人新建聚合模块的根本原因:聚合模块需要有一个合理的<dependencies>,把这些业务模块真正"依赖"进来,report-aggregate才有东西可扫。
4.3 标准解法:新增聚合模块
我的习惯是在多模块工程里额外加一个专门的聚合模块,名字通常叫coverage-report或aggregation,packaging设为pom。它的完整配置如下:
<project> <parent> <groupId>com.example</groupId> <artifactId>mall-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <artifactId>coverage-report</artifactId> <packaging>pom</packaging> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>mall-common</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>mall-service</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>mall-web</artifactId> <version>${project.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>aggregate-report</id> <phase>verify</phase> <goals> <goal>report-aggregate</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>注意:coverage-report模块不需要写<modules>,它只需要声明依赖。构建时在根目录执行mvn clean verify,Maven 的 reactor 会按模块依赖顺序构建:先构建mall-common、mall-service、mall-web,最后构建coverage-report。到了coverage-report的verify阶段,report-aggregate就能在当前构建上下文里找到这几个子模块的数据,汇总出一份完整报告。
最终报告输出在coverage-report/target/site/jacoco-aggregate/index.html,打开就是全模块总览,可以点击跳转到任意一个类的覆盖率详情。
4.4 不新增模块时怎么处理
如果你的项目结构已经定型,不方便新增模块,也有变通方案:找一个"恰好依赖了所有需要统计模块"的业务模块,把report-aggregate配置在那个模块上。比如mall-web依赖了mall-service和mall-common,那直接在mall-web上执行report-aggregate,就能聚合mall-web自身和它依赖的所有模块。
但这种方式有两个隐患:一是报告位置在业务模块的target/site下,很容易在后续构建中被覆盖或清理;二是如果以后某个模块不再被mall-web依赖,它就会静默地从聚合报告里消失。所以我还是建议新增专门的聚合模块,职责单一,不会被业务依赖变更误伤。
提示:如果聚合报告里出现了大量第三方库的类(比如 Spring 框架内部的类),可以在
report-aggregate的<configuration>里加<excludes>过滤掉,避免分母把整体覆盖率拉低。
5. 坑点四:class 与 source 路径解析不准,覆盖率数字里藏着"幽灵源码"
5.1 现象:数字在、源码失联
第三种坑最让人困惑的地方在于,报告的覆盖率数字看起来是正常的,有百分比、有类列表、甚至能显示某个类的 80% 行被覆盖。但你想点进去看看哪些行没覆盖,却提示找不到源码文件。或者更诡异一点,同一个类在单模块报告里一切正常,在聚合报告里却全部显示红色,指令覆盖率归零。
出现这种情况,基本可以断定是report-aggregate在解析 class 或 source 路径时出了问题。它虽然从依赖模块找到了exec文件,但没能正确关联到对应的 class 文件和 source 文件,于是报告只能显示一个总覆盖率轮廓,无法展示源码级别的行覆盖。
5.2 排查链路:从 jacoco.xml 里找线索
JaCoCo 的 HTML 报告本质上是由jacoco.xml渲染出来的。遇到源码不显示的问题,别急着眼花缭乱翻 HTML,直接打开聚合目录下的jacoco.xml搜关键信息。
jacoco.xml里每个类会记录它的class文件路径,每个<sourcefile>节点会记录源码文件名。你搜索一个出问题的类,看它对应的<sourcefile name="OrderService.java"/>是否存在。如果sourcefile节点不存在,说明聚合时压根没找到这个类的源码文件。
接下来查看聚合报告target/site/jacoco-aggregate下有没有src目录,以及目录里的包结构是否完整。如果src目录下是空的,那就说明 source 路径没有传进来。
另一边,检查 class 文件的解析源。这里有一个很容易被忽略的坑:report-aggregate在读取依赖模块的 class 时,优先使用当前 reactor 上下文中模块的输出目录。如果你不在根目录执行全量构建,而是在聚合模块目录下单独执行mvn verify,Maven 的 reactor 里只有coverage-report和它的父 POM,其他业务模块不会出现在 reactor 中。这时候report-aggregate只能去本地仓库找依赖 jar,从 jar 里读 class。本地仓库的 jar 可能是很久之前 install 的旧版本,和你正在分析的项目代码对不上,覆盖率自然不可信。
5.3 解法与习惯建议
标准解法其实很简单:始终在根目录执行全量构建命令:
mvn clean verify不要只跑到mvn test,也不要只跑到某个模块目录下单独执行verify。report-aggregate绑定在verify阶段,全量构建能保证所有模块的exec、class、source都在同一个 reactor 上下文中,聚合时不会因为缺文件而静默降级。
如果你的项目确实用了非标准目录,比如 Kotlin 项目把源码放在src/main/kotlin,正常情况下kotlin-maven-plugin会把src/main/kotlin加入 Maven 的 compile source root,JaCoCo 能直接识别。但如果某个模块是自己用脚本或者build-helper-maven-plugin添加的额外源码目录,聚合时可能识别不到。这种情况下可以给report-aggregate显式配置源码目录。
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>aggregate-report</id> <phase>verify</phase> <goals> <goal>report-aggregate</goal> </goals> <configuration> <sourceDirectories> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> </sourceDirectories> </configuration> </execution> </executions> </plugin>依赖 Lombok 的项目这里也要多说一句:Lombok 会在编译期生成大量样板方法,JaCoCo 会把这些方法也统计进覆盖率。我见过不少团队因为 Lombok 生成的equals、hashCode没被测试覆盖,导致项目的整体覆盖率莫名低了几个百分点。这类问题虽然不是路径解析直接引起的,但如果聚合报告里"幽灵类"特别多,很可能就是 Lombok 生成的代码。建议在 JaCoCo 配置里统一排除:
<configuration> <excludes> <exclude>lombok.*</exclude> <exclude>**/generated/**</exclude> </excludes> </configuration>注意这个<excludes>配置同时作用于 class 分析和报告展示,能有效过滤掉那些不属于你业务代码的噪音类。
6. 坑点五:版本与构建时序失控,数据互相不认账
6.1 现象:换台机器、换个流水线结果就变了
最后这个坑更像"慢性病"。它的典型表现是:本地执行mvn verify生成的聚合报告数据是对的,但 CI 上跑出来的覆盖率比本地低,或者缺失了某几个模块的数据。有时候同一个 CI 流水线,上一次跑和下一次跑结果还不一样。
这种"数据互相不认账"的问题,根因通常不是某个单一配置,而是版本一致性和构建时序同时失控。
6.2 排查链路:exec 时间戳、effective-pom、skip 开关
遇到结果不稳定,按顺序查三样东西:
第一步,先看所有模块的exec文件。执行:
find . -name "*.exec" -exec ls -l {} \;重点关注每个exec的时间戳和文件大小。如果某个模块的exec时间戳明显早于这次构建,说明这个模块这次压根没跑测试,或者测试中途被跳过了。如果某个模块的exec大小只有几百字节,而其他模块都有几十 KB,说明它的 agent 采集到的覆盖率数据严重偏少,基本可以断定测试没有真正执行或者模块内有skip配置。
第二步,检查每个模块实际生效的插件版本。执行:
mvn help:effective-pom -Dverbose看各个模块的jacoco-maven-plugin和maven-surefire-plugin版本。如果不同模块显示的版本不一样,优先检查父 POM 是否用了pluginManagement。pluginManagement只对声明了对应插件的子模块生效;如果子模块自己单独声明了插件版本,就会覆盖父 POM 的管理。
第三步,检查skip开关。多模块工程里很容易出现某些模块为了"加速构建"配置了跳过测试:
<properties> <maven.test.skip>true</maven.test.skip> <jacoco.skip>true</jacoco.skip> </properties>maven.test.skip会跳过测试编译,当然也就没有覆盖率数据。jacoco.skip会跳过 JaCoCo 的 agent 注入,测试会跑,但不会生成exec。这两种情况都不会让聚合报告报错,只是相应模块的数据从聚合里消失,然后整体覆盖率被剩余模块拉平。
6.3 标准解法:统一版本、统一时序、必要时用 merge
解法其实不复杂,核心是两条原则。
第一,版本必须统一在父 POM 的pluginManagement里。比如:
<pluginManagement> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.12</version> </plugin> </plugins> </pluginManagement>子模块不要自己声明版本,只声明<goal>。这样所有模块的 agent 和 report 都是同一版本,exec 文件的解析行为一致。
第二,时序必须统一。报告生成依赖测试已经跑完,所以聚合目标必须绑定在verify阶段,并且整个构建过程应该是一次完整的mvn clean verify。不要在 CI 里把"跑测试"和"生成报告"拆成两个独立 Job,除非你能保证两个 Job 之间共享完整的构建产物文件。
如果你确实因为多阶段流水线等原因必须拆分,可以考虑用merge目标把多个模块的exec合并成一个文件。典型配置如下:
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>merge-results</id> <phase>verify</phase> <goals> <goal>merge</goal> </goals> <configuration> <fileSets> <fileSet> <directory>${project.basedir}</directory> <includes> <include>**/target/jacoco.exec</include> </includes> </fileSet> </fileSets> <destFile>${project.build.directory}/jacoco-aggregate.exec</destFile> </configuration> </execution> </executions> </plugin>但merge目标只能合并exec数据,它不负责收集 class 和 source。合并后的exec还需要配合report目标、并指定好 class 目录和 source 目录,才能生成可读的聚合报告。所以merge更适合"我已经有一份全部模块的 exec,只是想合并体积或转换格式"的场景,日常多模块聚合还是优先用report-aggregate。
6.4 别忘了 SonarQube 的场景
如果你的覆盖率最终要推送到 SonarQube 之类平台,还有一个相关的小坑:SonarQube 扫描时并不会自动读取 JaCoCo 的聚合报告,你需要显式告诉它 exec 文件的路径。多模块工程里通常这样配:
<properties> <sonar.jacoco.reportPaths> ${project.basedir}/../mall-common/target/jacoco.exec, ${project.basedir}/../mall-service/target/jacoco.exec, ${project.basedir}/../mall-web/target/jacoco.exec </sonar.jacoco.reportPaths> </properties>如果只填了根目录的target/jacoco.exec,SonarQube 扫描多模块时大概率只能拿到一个模块的数据,这也是"报告和平台数据对不上"的常见原因。
7. 给团队的快速排查清单:从症状直接锁定根因
7.1 速查表
最后把五个坑浓缩成一张速查表,排查的时候对应着看,能省不少时间。
| 症状 | 最可能的根因 | 第一条检查命令 |
|---|---|---|
| 报告空白或全部 0% | argLine被覆盖,agent 没注入 | find . -name "*.exec" |
| 报告只有单模块数据 | report和report-aggregate混用 | 看target/site目录名 |
| 报告缺了部分模块 | 聚合模块没有声明对应依赖 | mvn dependency:tree |
| 覆盖率数字有但源码不显示 | source/class 路径解析失效或 reactor 不完整 | 打开jacoco.xml搜sourcefile |
| 本地正常、CI 结果不一致 | 插件版本不统一、构建时序拆分、skip 误配 | mvn help:effective-pom加-Dverbose |
7.2 让报告生成保持稳定的团队约定
排查过这么多轮之后,我现在的习惯是给团队立几条硬约定:
- 新增一个业务模块时,必须同步把它加进聚合模块的
<dependencies>。曾经我见过一个模块测试覆盖率接近 90%,但因为忘了加依赖,聚合报告连续两周都显示它是 0%。这事最坑的地方在于,没人会主动发现,因为它不报错。 - 任何人在子模块里都不要单独执行
mvn jacoco:report。统一由聚合模块的verify阶段产出报告。如果确实要单独看某个模块的覆盖率,可以用 IDE 的 JaCoCo 插件,而不是去手动触发 Maven 目标。 - CI 流水线里固定执行
mvn clean verify,不要拆开。拆开跑测试再跑报告,短期看能省时间,长期就是给自己埋坑。 - 版本升级时,先升父 POM 的
pluginManagement,再跑一次全量验证,不要只在某个子模块里手动改版本。
我在实际项目里还发现一个小技巧值得分享:给聚合报告做成 CI 产物存档,并在流水线脚本里加一个简单检查——如果jacoco-aggregate目录不存在,直接让流水线失败。这些看起来"多余"的检查,恰恰能在问题发生第一天就暴露出来,而不是等到发布前才发现覆盖率一直不对。毕竟,覆盖率报告的存在价值是让人信任它,如果连聚合数据的完整性都没法保证,那份报告反而不如不生成。