上周在本地联调一个多模块项目时,我被 Maven 的打包时长搞得有点崩溃:父 pom 下面挂了六个子模块,每次执行一次全量 package,光是单元测试加起来就能跑掉近两分钟,其中还包括一些会去连测试数据库的用例。更麻烦的是,有时我只是想单独验证某个模块的改动,结果整个 reactor 先把所有模块都跑了一遍,用不上的模块也跟着编译、测试、打 jar。后来我把“跳过单元测试”和“排除某些项目”这两件事彻底摸清以后,本地构建从两分钟降到了二十秒左右。这篇就把我实际用到的参数、组合命令和踩过的一些坑整理出来。
1. 跳过测试的第一个分岔路:skipTests 和 maven.test.skip 差在“编译”这一步
1.1 从 Maven 构建生命周期理解两个参数
先看 Maven 的构建生命周期。一个 Java 项目从源码到最终打包,至少要经过这几个阶段:compile→test-compile→test→package。其中test-compile负责把测试代码编译成字节码,test阶段才真正执行测试用例。默认情况下这两个阶段都会跑,而你要“跳过单元测试”,本质上有两种做法:
-DskipTests:跳过test阶段的测试执行,但是仍然会执行test-compile,也就是测试代码依然会被编译。-Dmaven.test.skip=true:跳过test-compile和test两个阶段,测试代码根本不会参与构建。
命令行示例是这样的:
# 只跳过测试执行,测试代码仍会编译 mvn clean package -DskipTests # 连测试代码一起跳过,相当于测试完全退出构建 mvn clean package -Dmaven.test.skip=true很多第一次接触 Maven 的朋友会以为-DskipTests就是“什么都不测、什么都不编译”,实际上它的工作范围只停留在“运行”这一层。这一点在本地打包时最容易造成误解,尤其是当测试代码写错了的时候。
1.2 记住:测试代码编译失败时,-DskipTests 救不了你
我遇到过不止一次这样的场景:本地改了业务代码,顺手改了一个测试类,但测试类里有一行语法错误或引用了不存在的方法。此时我用mvn clean package -DskipTests去打包,结果构建仍然在test-compile阶段就挂掉了,报错信息指向某个测试类。
一开始我以为是参数没生效,后来才想明白:-DskipTests只是不执行测试,不代表不编译测试代码。只要测试代码编译失败,构建一样会终止。所以如果在本地只想快速拿到可运行的 jar,对测试代码的编译结果并不关心,那就应该用更彻底的参数:
mvn clean package -Dmaven.test.skip=true这里有一个值得注意的取舍:-Dmaven.test.skip=true会让测试代码完全不参与构建,好处是快,坏处是如果你希望顺手检查一下测试代码能不能编译通过,那么这个参数会把这个检查机会也一并抹掉。所以我的习惯是:
- 平时只想省时间、不想跑测试:用
-DskipTests。 - 测试代码本身已经有编译错误、但我当前只关心主代码打包:用
-Dmaven.test.skip=true。
1.3 “没有测试也要跑”时的 failIfNoTests=false
另一个容易被忽略的问题是:有些模块可能没有任何测试类。如果 Maven 配置了默认的测试发现规则,当某个模块“没有测试可执行”时,Surefire 插件会直接报No tests were executed!,构建以失败告终。这种情况在细分子模块的仓库里很常见,因为公共模块、工具模块可能确实没有写单测。
解决方式就是显式告诉 Maven“没有测试也不许报错”:
mvn clean package -DskipTests -DfailIfNoTests=false这个参数对surefire和failsafe插件都生效。把-DfailIfNoTests=false和-DskipTests搭配使用,是我本地打包时的默认组合之一。
2. 只想跳过单元测试还是集成测试:surefire 和 failsafe 要分开看
2.1 两类测试插件各自认的参数
Maven 项目里的测试并不只有一种。最常用的两类测试由两个插件分别负责:
maven-surefire-plugin:负责执行单元测试,默认绑定在test阶段,类名一般是*Test、Test*等。maven-failsafe-plugin:负责执行集成测试,默认绑定在integration-test和verify阶段,类名一般是*IT、*ITCase等。
-DskipTests是一个全局性的开关,它会同时影响 surefire 和 failsafe。但如果你只是想跳过其中一类测试,保留另一类,就需要更精确的参数:
# 只跳过单元测试,保留集成测试 mvn clean verify -DskipUTs # 只跳过集成测试,保留单元测试 mvn clean verify -DskipITs这里有个细节:-DskipTests在某些旧版本项目或复杂插件配置下,可能会对 failsafe 产生“预期之外的跳过效果”。所以当项目里同时存在单测和集成测试时,我不建议只依赖一个笼统的-DskipTests,而是根据当前要跑哪一类测试来选择参数。
2.2 区分场景:本地跑集成测试,但跳过慢的单测
本地开发时有一种很典型的需求:集成测试通常用来验证跨模块的接口和外部依赖,运行时间往往比单元测试短,而且对本地调试更有参考价值;单元测试数量多、跑起来慢,其中还有一部分用例需要准备大量 mock 数据。此时我通常只跳过 unit test,保留 integration test:
mvn clean verify -DskipUTs不过这个参数在较新的 surefire/failsafe 版本里支持得比较好。如果你用的 Maven 插件版本比较老,或者想兼容性更强,也可以用主动配置插件skip属性的方式。但一般我建议优先尝试命令行参数,命令行的优先级高于 pom 中的默认配置。
2.3 不要轻易把 skip 参数写进 pom.xml——它比命令行更危险
命令行参数是“一次性”的,只影响当前这一次构建。但如果为了省事,直接把跳过测试的配置写进 pom.xml,影响范围就完全不同了。比如有人会把这样一段配置加到项目里:
<properties> <maven.test.skip>true</maven.test.skip> </properties>这行配置一旦提交,意味着所有人、所有环境构建时都会跳过测试,包括本地 IDE 里的构建、CI 上的流水线、发布前的验证构建。等发现测试已经很久没跑过时,通常已经积累了一批坏用例,修复成本很高。
我见过一次真实的“翻车”经历:一位同事为了本地调试方便,在父 pom 的 properties 里临时加了<maven.test.skip>true</maven.test.skip>,调完代码后忘记还原就提交了。接下来的两周里,项目构建全都非常顺利,因为测试根本没执行。直到上线前手动跑了一次全量测试,才发现有十几个用例早就挂了。所以我的原则是:pom 里永远不写和skip相关的测试属性,所有跳过一次性的跳过操作都通过命令行完成。
3. 多模块项目里“排除某个项目打包”的实际做法
3.1 Maven 没有 -x,排除要靠“只选择”来实现
用惯了某些构建工具的朋友,可能习惯用类似-x 任务名的方式排除某个任务。Maven 的命令行里没有直接的“排除某个模块”参数,所以“排除某个项目进行打包”在 Maven 里通常等价于:只选择需要参与构建的那几个项目。最简单的就是-pl参数。
假设父 pom 下有以下模块:
parent-pom ├── common-utils ├── core-service ├── admin-service ├── web-service └── report-service如果我只想打包两个模块,比如common-utils和web-service,可以在父 pom 目录下执行:
mvn clean package -pl common-utils,web-service通过逗号分隔指定模块名,Maven 就只会把这两个模块放进 reactor 里构建。其余模块即使写在了父 pom 的<modules>里,这次构建也不会碰它们。这就是 Maven 语境下“排除某个项目打包”最常见的实现方式。
3.2 -pl / -am / -amd:选项目也要照顾依赖链
直接列-pl虽然简单,但有一个前提需要关注:模块之间的依赖关系。比如web-service依赖core-service,你只执行-pl web-service,Maven 会尝试从本地仓库去找core-service的 jar。如果这个 jar 以前从来没有被安装过,或者本地仓库里还是旧版本,构建就会失败或使用过期版本。
为了解决这个问题,Maven 提供了另一个参数-am,即--also-make,它的意思是“把当前所选模块依赖的上游模块也一起构建”。经典用法:
mvn clean install -pl web-service -am这条命令会把web-service以及它依赖的所有上游模块全部拉进本次 reactor 构建,并且按照依赖顺序依次构建。这样既达到了“只构建我关心的模块和必要依赖”的目的,也不会因为依赖缺失而报错。
反过来还有一个参数-amd,即--also-make-dependents,意思是“把依赖当前所选模块的下游模块也一起构建”。这个参数更多用于回归验证场景:比如你改了core-service,想知道哪些依赖它的服务会被影响,就可以执行:
mvn clean install -pl core-service -amd实际项目中,我用到最多的是-pl xxx -am,它能让“排除无关项目 + 补齐必要依赖”一次性完成。
3.3 实践中最常用的组合命令
多模块仓库里,本地常规开发打包,我的默认命令基本是这样:
mvn clean install -pl web-service -am -DskipTests -DfailIfNoTests=false这条命令的意图非常明确:
-pl web-service:只把web-service列为本次构建的目标。-am:自动带上web-service依赖的上游模块。-DskipTests:不执行测试,节省时间。-DfailIfNoTests=false:避免某些下游模块没有测试类时报错。
用这条命令之后,其他与web-service没有依赖关系的模块就被“排除”在构建之外了。如果你担心某次构建忘了包含某个模块,可以在命令最后加一个-DskipTests之外的手段来确认,后面我会专门讲怎么看构建结果。
3.4 临时注释 modules 的代价,比想象中大
还有一种“排除某个项目”的做法是:直接去父 pom 的<modules>节点里,把某个<module>临时注释掉。
<modules> <module>common-utils</module> <module>core-service</module> <!-- <module>admin-service</module> --> <module>web-service</module> </modules>这种做法在本地单机调试时确实有效,构建时间也能立刻降下来。但它有一个很大的隐患:一旦注释忘了还原,提交到代码仓库后,所有人对这个模块的印象都会停留在“它好像不参与构建了”。如果有人在那个模块里提交了新代码,CI 根本不会构建它,问题会一直到发版时才暴露。
相比直接改<modules>,我更推荐用-pl加上-am的组合。命令是临时的,不会影响仓库状态,也不会影响同事和 CI。通过命令行的“只选择”,一样能达到“排除”的效果。
4. 测试失败也不想让构建停住:failure.ignore 和跳过是两码事
4.1 failure.ignore 到底干了什么
如果你在团队里看过别人写的打包命令,可能会发现这样一个参数:
mvn clean package -Dmaven.test.failure.ignore=true第一眼看上去,它像是在“跳过测试失败”。严格来说它并不是:测试仍然会真的执行,只是在部分测试失败时,构建不会中断,而是继续往下走,最后整个构建可能显示BUILD SUCCESS。测试结果会被保留在target/surefire-reports目录下,但不会阻碍 jar 包生成。
这个参数和-DskipTests有本质区别:
| 参数 | 测试是否执行 | 测试失败是否影响构建 | 适用场景 |
|---|---|---|---|
-DskipTests | 否 | 不影响,因为根本没跑 | 本地快速打包、临时跳过测试 |
-Dmaven.test.failure.ignore=true | 是 | 不阻断构建 | 想在失败中继续打包,同时保留测试报告 |
4.2 什么时候用它,什么时候用 skip
我自己的使用习惯是这样的:如果只是想快速拿到一个可运行的 jar,不关心测试结果,那就用-DskipTests,因为连测试执行时间都省了。但如果测试用例已经在持续集成环境里跑过,本地只是想顺带看一眼测试结果,同时又不希望某个已知失败用例阻塞打包,那才考虑-Dmaven.test.failure.ignore=true。
需要提醒的是,failure.ignore会让构建产物包含“未通过测试验证”的代码。如果项目对交付质量有严格要求,这种产物不应该用于发布。我个人的红线条是:本地联调可以用,提测和上线绝对不开。
4.3 一个被我误用的历史教训
早些时候我接手过一个老项目,里面的一些测试因为环境问题经常失败。当时我想节省时间,就在命令里加了-Dmaven.test.failure.ignore=true,结果确实能让打包顺利完成。但随着时间推移,团队成员开始习惯性带上这个参数,每次构建看起来都是绿色成功,实际上测试报告里失败用例越来越多。后来项目换到一个干净环境重新跑测试,才暴露出很多基础逻辑问题。
这不是参数本身的问题,而是没有搞清楚它和“跳过测试”的区别。如果你只希望“不跑测试”,就用-DskipTests;如果你必须“跑了测试但不让失败中断”,才用failure.ignore。两者不要混为一谈。
5. 三个打包翻车现场,完整排查链路复盘
5.1 场景一:测试代码编译报错,止步于 test-compile
某次我在本地改完业务代码,执行了:
mvn clean package -pl web-service -am -DskipTests结果构建失败,错误信息停留在test-compile阶段,点开日志发现是一个测试类里引用了一个被删除的方法。
排查过程并不复杂:先看 Maven 日志的BUILD FAILURE位置,发现不是编译主代码报错,而是编译测试代码报错。此时我意识到-DskipTests并不会跳过测试代码编译,于是换成了更彻底的参数:
mvn clean package -pl web-service -am -Dmaven.test.skip=true构建立即通过。这件事给我留下的经验是:如果在本地只是图快,不在乎测试类的编译状态,直接用-Dmaven.test.skip=true会更省心;但如果你希望测试代码始终编译通过,那么-DskipTests反而是更好的选择,因为它在不执行测试的同时,保留了编译检查。
5.2 场景二:排除了公共模块后,启动时找不到类
有一次我只想打包一个服务模块,觉得它的公共依赖模块没什么改动,为了图快就没有把公共模块放进构建列表,命令是:
mvn clean package -pl web-service结果虽然构建成功了,但运行时抛了NoClassDefFoundError。排查了一圈发现,本地仓库里的公共模块 jar 是两周前的旧版本,代码里新加的类和方法全都没有。问题不是打包失败,而是“打包成功但打进了旧依赖”。
这个问题的根子在于:web-service依赖的公共模块没有被本次构建更新到本地仓库。改法就是加上-am,让依赖模块也一起构建并安装:
mvn clean install -pl web-service -am从此之后,我再也不单独用不带-am的-pl去构建多模块项目里的某个模块,特别是当这个模块有很活跃的上游依赖时。
5.3 场景三:顺手注释 modules,三个月没人发现模块没在构建
还有一个案例不涉及命令行,而是上面提到过的“注释 modules”操作。有同事为了本地启动快,在父 pom 里把一个长时间不用的模块给注释了,后来开发工作收尾,这个模块也该重新参与构建,但大家忘了还原。结果这个模块的新代码连续三次提交都没通过 CI 构建验证,发版前准备打包时才发现少了关键功能。
这个问题的排查链路也很典型:先看目标 jar 包是否存在,发现 jar 里的类是最早之前的版本;再看 CI 日志,构建过程中根本没有出现该模块;最后才定位到父 pom 的 modules 被注释。
之后我们达成了一个约定:任何“临时排除模块”的操作都通过-pl完成,不改仓库里的 pom 文件。如果确实需要长期从默认构建里排除某个模块,那也要单独开分支、写清楚提交信息,而不是悄无声息地注释掉。
6. 本地打包真正能提速的四个小习惯
6.1 先 install 还是先 package,多模块项目别搞反
在单模块项目里,mvn package就能产出 jar。但在多模块项目里,package和install的差别直接影响依赖解析:package只是把当前模块的 jar 生成到target目录,不会放进本地仓库;install会把模块安装到本地 Maven 仓库。如果别的模块依赖这个模块,而你只执行了package,对方在构建时仍然会去本地仓库找旧版本。
所以我在本地处理多模块项目时,命令一般优先选install,而不是package。配合-pl xxx -am,即使只改了链路里的一部分模块,其他模块也能用到最新的本地 jar。
6.2 用 -T 并行和 -o 离线提升本地效率
Maven 3.3 以上版本支持-T参数做并行构建。比如:
mvn clean install -T 1C -DskipTests1C表示每个 CPU 核心分配一个线程。多模块项目里并行构建的提速效果比较明显,特别是当模块之间没有复杂依赖时。不过并行构建对机器性能和模块依赖关系有一定要求,首次使用时建议先小范围试跑,确认没问题再常态使用。
如果本地依赖已经下载完整,另一个提速方式是-o离线模式:
mvn clean install -o -DskipTests离线模式不会访问远程仓库,能省掉大量网络请求时间。缺点是有依赖没下载全时会直接报错,所以我一般只在依赖很稳定的阶段才加-o。
6.3 临时代码、临时 skip 都要有明确的“还原点”
无论是测试里的临时打印,还是命令行里临时加的 skip 参数,本质上都是“为了让本地更快”而引入的临时状态。最容易出问题的往往不是命令,而是临时状态被带到不该带的地方。我现在的做法是:
- 命令行参数只加在本地执行的那一条命令里,不加进任何脚本。
- 如果确实需要在 IDEA 的 Maven 面板里临时配置 skip,也会在调试结束后的第一件事就是把它去掉。
- 任何和 pom 相关的改动,哪怕只是加一个空格,都会慎重考虑它会被谁、在哪个环境读到。
6.4 看 Reactor Summary 确认“真的没包含那个模块”
多模块构建结束后,Maven 会打印一段Reactor Summary,把本次参与构建的所有模块、构建结果列出来。很多人在构建成功后根本不会看它,但这恰恰是快速确认“排除某个项目是否生效”最好的地方。
比如我执行:
mvn clean install -pl web-service -am -DskipTests构建结束后,我扫一眼 Reactor Summary 里的模块列表,确认里面只有web-service和它的上游依赖,没有其他无关模块,这次构建就符合预期。如果发现某个意外模块出现在列表里,就说明-pl参数的模块名写错了,或者依赖关系超出了预期。
到了这一步,本地打包效率基本的优化思路就清楚了:先用-pl和-am控制构建范围,再用-DskipTests或-Dmaven.test.skip=true控制测试参与程度,最后用-DfailIfNoTests=false避免“没有测试类”的报错。这几件事组合在一起,就是我在多模块项目里本地打包的一整套基础操作。如果以后再遇到“构建又慢又跑一堆无关测试”的情况,不妨先检查一下自己是不是还在用最原始的全量命令。