Maven 构建提速:这 5 个配置让打包时间从 5 分钟降到 30 秒

引言

改一行代码验证一下,等 Maven 打包 5 分钟——一天改 20 次,一个半小时就花在盯着进度条上。这不是段子,是我们一个 40 模块的 Spring Cloud 项目的真实日常:mvn clean package 全量跑一次 4 分 50 秒,其中测试 1 分 20 秒、Checkstyle 30 秒、重复下载插件 40 秒、真正的编译只占 40 秒,剩下的时间全在串行等待。

更气人的是这些时间大多是白花的:模块之间明明没有依赖关系却串行构建;只改了一个类却全量重编译;本地仓库明明有依赖还要去远程检查更新。我们花了一个下午做了五件事,本地打包从 5 分钟降到了 30 秒左右(全量冷构建 1 分 10 秒),CI 平均构建从 6 分钟降到 1 分 40 秒。这篇文章把五个手段从配置到坑点一次讲清——它们不是互斥的银弹,而是分别打掉耗时构成里不同的部分。

先看我们的耗时构成基线(提速前,40 模块,MacBook Pro M1 Pro):

全量 mvn clean package:4分50秒
├─ 依赖解析/插件下载检查      40s
├─ 编译(大量模块串行+全量)   1分50秒
├─ 单元测试                  1分20秒
├─ Checkstyle/SpotBugs       30s
├─ Spring Boot 重打包/镜像    30s
└─ 其他等待(串行空隙)        约1分钟

一、手段 1:并行构建 -T 4C(打掉模块间的串行等待)

1.1 默认行为:reactor 是串行的

Maven 多模块构建默认按 reactor 顺序一个模块一个模块构建。但实际上没有依赖关系的模块完全可以并行:

# 按 CPU 核数的 1 倍开工作线程(推荐起步值)
mvn clean package -T 1C

# 每个 CPU 核心 4 个线程(IO 等待多、模块独立性强时)
mvn clean package -T 4C

# 或直接指定线程数
mvn clean package -T 8

-T 4C 的含义是 4 × CPU 核数。注意不是"用 4 个线程"——8 核机器上实际开 32 个工作线程。

1.2 前提:有依赖关系的模块依然会被正确串行

模块依赖关系:
  common ← user-api ← user-service
         ← order-api ← order-service

并行后实际执行:
  时间片1:common(所有模块都依赖它,必须最先)
  时间片2:user-api、order-api(互不依赖,并行)
  时间片3:user-service、order-service(互不依赖,并行)

Maven 保证被依赖模块先构建完才会开始依赖方——并行的是"同一层互不依赖的模块",不会破坏构建正确性。

1.3 实测与坑

配置40 模块构建耗时说明
默认串行4:50基线
-T 1C1:55收益最大的一步
-T 4C1:40提升变小,CPU 已饱和
-T 8C1:45反而略慢,线程切换+磁盘 IO 争抢

三个坑:

  1. 线程不安全的插件会随机失败:个别老旧插件(某些代码生成器、自定义插件)内部有共享静态状态,并行下偶发 NPE/文件覆盖。遇到时用 -T 1C 起步,或对问题模块单独串行(mvn -pl problem-module package)。
  2. 日志交错难读:多模块输出混在一起。加 -l build.log 输出到文件再排查,CI 上尤其建议。
  3. clean 阶段并行删除可能撞 target 目录:极少见,遇到时把 clean 单独跑(mvn clean && mvn package -T 1C)。

经验值:编译密集的项目 -T 1C 性价比最高;IO/等待密集(大量测试、网络资源)可以试 -T 2C;-T 4C 只在模块数多(>30)且机器核数富余时才有意义。

1.4 固化到配置,不用每次手敲

<!-- 根 pom.xml 的 maven.config 同级目录:.mvn/maven.config -->
<!-- 文件内容(每行一个参数,对所有 mvn 命令默认生效) -->
-T 1C

在项目根目录建 .mvn/maven.config 文件写入 -T 1C,团队所有人和 CI 不用传参就默认并行——配置进仓库比写进文档有效 100 倍。


二、手段 2:增量编译(打掉"改一行重编全部")

2.1 maven-compiler-plugin 的增量机制

现代 maven-compiler-plugin(3.1+)默认就开启了增量编译(useIncrementalCompilation=true):只重新编译"自上次构建后变化的源码 + 受影响的类"。但它的增量判定比较保守,有两个常见的"假增量"场景:

场景现象原因
改了一个 public 类整个模块全量重编该类被大量类引用,保守判定全部受影响(正确行为)
任何输入文件时间戳变化全量重编staleMillis(默认 0):时间戳不同就判定过期,git checkout/build 后必全量
注解处理器(Lombok/MapStruct)经常全量处理器声称影响所有类

2.2 推荐配置

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.13.0</version>
    <configuration>
        <release>17</release>
        <!-- 增量编译(默认即 true,显式声明显式意图,防老版本默认值差异) -->
        <useIncrementalCompilation>true</useIncrementalCompilation>
        <staleMillis>100</staleMillis>
        <!-- 增量编译下 annotation processing 建议显式声明路径,避免全量扫描 -->
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.34</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

staleMillis=100 表示时间戳差异在 100ms 内视为未变化——消除文件系统时间戳精度/检出导致的无谓重编。

2.3 本地开发最有效的习惯:不要 clean

# ❌ 本地每次都 clean:target 删光,必然全量编译
mvn clean package

# ✅ 日常迭代:保留 target,增量编译只编变化的类
mvn package
mvn compile

clean 的语义是"我不相信增量了,全部重来"——它是排查问题时的消毒剂,不是日常操作。我们团队把本地命令从 clean package 改成 package 后,单模块增量编译普遍从 20s+ 降到 3~5s。只在以下情况需要 clean:

  • 切换了分支/依赖版本,怀疑产物脏了
  • 注解处理器行为异常、类找不到
  • 发布构建(CI 建议保留 clean 保证干净环境)

2.4 实测

场景clean package增量 package(改 1 个类)
单模块(10 万行代码)28s4s
40 模块全量改一个模块1:40(配合 -T 1C)22s(只重编受影响模块链)

三、手段 3:本地跳过非必要插件(打掉测试和静态检查的等待)

3.1 最常用的两个开关,先搞清区别

# 跳过测试执行,但测试代码仍然编译(能发现测试代码编译错误)
mvn package -DskipTests

# 连测试代码都不编译(最彻底,本地赶时间用)
mvn package -Dmaven.test.skip=true
参数编译测试代码执行测试适用
-DskipTests✅❌本地日常首选,测试代码错了仍能发现
-Dmaven.test.skip=true❌❌极端赶时间/测试代码本身编译不过临时绕过
无参数✅✅CI、发版

3.2 用 Profile 管理本地/CI 差异(推荐做法)

不要让每个人记参数,在根 pom 定义 profile:

<profiles>
    <!-- 本地开发默认激活:跳过测试执行和重型检查 -->
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <skipTests>true</skipTests>
            <checkstyle.skip>true</checkstyle.skip>
            <spotbugs.skip>true</spotbugs.skip>
            <jacoco.skip>true</jacoco.skip>
        </properties>
    </profile>

    <!-- CI 显式激活:全部检查照跑 -->
    <profile>
        <id>ci</id>
        <properties>
            <skipTests>false</skipTests>
            <checkstyle.skip>false</checkstyle.skip>
            <spotbugs.skip>false</spotbugs.skip>
            <jacoco.skip>false</jacoco.skip>
        </properties>
    </profile>
</profiles>
# 本地:默认 dev,直接 package 就是跳过检查的快速构建
mvn package

# CI:显式 -Pci,所有质量门禁一个不少
mvn clean package -Pci

这比口头约定"大家本地记得加 -DskipTests"可靠得多——默认值就是安全且快速的,CI 上显式打开全部检查。

3.3 Checkstyle/SpotBugs 也支持增量

Checkstyle 插件本身有 sourceDirectories 全量扫描的开销,本地跳过最直接;如果团队希望本地也保留风格检查,可以配 IDE 插件实时检查(IDEA 的 Checkstyle 插件),把 Maven 执行只留给 CI——人机分工:IDE 实时反馈,CI 统一门禁。

3.4 实测(本地构建,40 模块)

配置耗时
全部检查+测试4:50
dev profile(跳测试/检查)1:05
dev profile + -T 1C + 增量约 30s

四、手段 4:离线模式 -o(打掉网络等待)

4.1 在线构建的隐性网络开销

即使依赖已经在本地仓库,Maven 默认仍可能发起网络请求:

  • SNAPSHOT 依赖/插件的更新检查(默认每次构建检查远程元数据)
  • 插件版本解析、仓库元数据下载
  • 网络不通/私服慢时,每个超时都要等(一次几秒到几十秒)
# 离线模式:只依赖本地仓库,零网络请求
mvn package -o

4.2 适用与前提

场景是否适合 -o
日常迭代,依赖早已下载齐✅ 最适合,构建时间稳定不抖动
CI 构建机(依赖已缓存)✅ 配合依赖预缓存使用
刚拉代码、新加入依赖、切了版本❌ 本地仓库没有会直接报错
首次构建新环境❌ 先在线跑一次预热仓库

典型工作流:新环境/新依赖先 mvn dependency:go-offline 预热(把依赖和插件一次性下载齐),之后日常构建全部 -o:

# 预热一次(依赖变更后再跑)
mvn dependency:go-offline

# 日常离线构建
mvn package -o -T 1C

4.3 SNAPSHOT 更新策略(治本)

频繁依赖内部 SNAPSHOT 包的团队,在线构建慢主要慢在更新检查。调整更新策略:

<!-- settings.xml 或 pom:从不自动检查 SNAPSHOT 更新,需要时手动 -U -->
<snapshots>
    <enabled>true</enabled>
    <updatePolicy>never</updatePolicy>
</snapshots>
# 确实要拉最新 SNAPSHOT 时手动强制更新
mvn package -U

updatePolicy=never + 手动 -U 的组合,把"每次构建都检查网络"变成"我需要时才检查"。

4.4 注意:离线模式不是缓存万能药

离线模式只解决网络等待,不能解决本地仓库污染——如果 ~/.m2 里混进了错误版本的内部 jar(install 了旧代码),离线只会稳定地用错包。遇到"代码改了行为没变",先怀疑本地仓库里的 SNAPSHOT 制品,mvn clean install -U 重新构建依赖链。这也是 CI 不建议长期开 -o 的原因之一(CI 应该用独立干净仓库 + 显式缓存策略)。


五、手段 5:构建缓存 + 镜像分层缓存(打掉重复工作和重复打包)

这一手段分两层:Maven 层的 Build Cache(缓存未改动模块的整个构建结果),和 Docker/Spring Boot 层的缓存(镜像构建不重复下载依赖)。

5.1 Maven Build Cache Extension(目标级缓存)

Maven 3.9+ 官方提供的 maven-build-cache-extension,比增量编译更激进:一个模块的源码、依赖、插件配置全都没变,就直接复用上次的构建产物(jar),连编译都跳过。多模块项目里你只改了 order-service,common 等 30 个没变的模块直接命中缓存。

启用只需一个文件 .mvn/extensions.xml:

<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0">
    <extension>
        <groupId>org.apache.maven.extensions</groupId>
        <artifactId>maven-build-cache-extension</artifactId>
        <version>1.0.0</version>
    </extension>
</extensions>

.mvn/maven-build-cache-config.xml(精简配置):

<cache>
    <configuration>
        <enabled>true</enabled>
        <hashAlgorithm>SHA-256</hashAlgorithm>
        <!-- 本地缓存目录 -->
        <local>
            <enabled>true</enabled>
        </local>
        <!-- CI 上可配远程缓存(HTTP 仓库),团队共享命中 -->
        <remote>
            <enabled>false</enabled>
            <url>${maven.multiModuleProjectDirectory}/.cache/remote</url>
        </remote>
        <!-- 哪些输入参与哈希:源码、pom、插件配置默认都算 -->
        <incremental>
            <enabled>true</enabled>
        </incremental>
    </configuration>
</cache>

效果(第二次构建,只改 1 个模块):

[INFO] Reactor Summary:
[INFO] common ............................. CACHED  SUCCESS   ← 整模块跳过
[INFO] user-api .......................... CACHED  SUCCESS
[INFO] order-api ......................... CACHED  SUCCESS
[INFO] order-service ..................... SUCCESS [4.2s]     ← 只有它真构建
构建方式改动 1 个模块的全量 package
增量编译22s(重编受影响链)
Build Cache8s(39 模块 CACHED,1 模块真实构建)

注意事项:① 缓存键包含 pom/源码哈希,改依赖版本自动失效,正确性有保障;② 生成代码类插件要确认其输入被纳入哈希(默认源码目录都算,生成到 target 的不影响);③ 远程缓存需要团队搭 HTTP 存储,本地开发先用本地缓存即可。

5.2 Spring Boot 分层包 + 构建镜像层缓存

很多团队用 spring-boot:build-image(Paketo Buildpacks)或 Dockerfile 构建镜像,慢在每次都重新下载/打包依赖层。

第一层:Spring Boot 分层 JAR(layers),把"依赖"和"应用类"拆到不同层:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>

Dockerfile 按依赖→应用的顺序分层,依赖不变时 Docker 层缓存直接命中:

FROM eclipse-temurin:17-jre AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17-jre
WORKDIR /app
# 依赖层(变化频率最低)先 COPY,命中缓存就不重新下载
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
# 应用类层(每次发布都变)放最后,只有这层重建
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
不做分层:每次镜像构建都把 200MB 依赖重新压一层 → 1~2 分钟
分层后:依赖层 99% 的发布命中缓存,只重建几 MB 的 application 层 → 10~20 秒

Buildpacks(mvn spring-boot:build-image)本身也利用分层和本地镜像缓存,配合 CI 持久化 /cache 目录(Paketo 的依赖缓存挂载点)效果相同。核心原则就一条:变化频率不同的东西放到不同层,稳定的层放前面。


六、五个手段的组合效果与落地顺序

6.1 各手段打掉的是耗时的哪一块

原始耗时 4:50
  -T 1C 并行         打掉模块串行空隙        → 1:55
  不 clean + 增量     打掉全量重编译          → 1:20
  dev profile 跳检查  打掉测试/Checkstyle     → 0:40
  -o 离线             打掉网络等待(抖动)     → 0:30(且时间稳定)
  Build Cache        未改模块整模块跳过       → 0:28
  镜像分层缓存        发布镜像阶段(另算)     1:30 → 0:20

6.2 推荐落地顺序(按投入产出比)

顺序动作成本本地耗时
1dev/ci profile + 日常不 clean改一次 pom,0 风险4:50 → 1:05
2.mvn/maven.config 写 -T 1C一个文件→ 0:40
3依赖预热后 -o + updatePolicy=never习惯调整→ 0:30
4Build Cache extension加 extensions.xml,小范围验证→ 0:28
5镜像分层 + CI 缓存持久化改 Dockerfile/CI 配置镜像 1:30→0:20

前两步零成本、当天生效、收益拿走 80%,是所有项目都应该立刻做的;后三步按团队成熟度推进。

6.3 CI 环境的对应配置(不能照搬本地)

手段本地CI
测试/检查dev profile 跳过必须全跑(-Pci)
并行-T 1C看 agent 核数,2~4 核小机器别盲目 -T 4C
增量/缓存本地 target 持续积累每次干净 checkout,靠 Build Cache 远程缓存 + 依赖缓存目录挂载
离线日常 -o预热缓存后可用 -o,但保留一次在线兜底
clean不要建议保留(保证产物干净)

CI 缓存的关键目录:~/.m2/repository(依赖)、.mvn/.cache(Build Cache)、Docker layer cache、Paketo /cache。


七、常见问题

7.1 为什么我配了 -T 4C 感觉没快多少?

三种常见原因:① 模块链是一条直线(A→B→C→…→Z 全互相依赖),没有可并行的层,并行退化为串行——这是项目结构问题,把大模块拆成平级模块才能受益;② 瓶颈是单个大模块的测试/IO,再多线程也只跑这一个模块,应该优化这个模块本身(测试并行、分片);③ 机器已经 CPU 打满(同时开 IDE、Docker),并行只增加争抢。先用串行构建看 Reactor Summary 里每模块耗时,确认瓶颈形态再调线程数。

7.2 增量编译老是"莫名其妙"全量重编?

按顺序排查:① 是否跑了 clean(或 IDE 自动 clean);② 是否有注解处理器(Lombok 正常,MapStruct 看版本,某些 querydsl/apt 插件会强制全量);③ staleMillis 调大容忍时间戳抖动;④ 模块间是否通过 target/classes 以外的目录互相引用文件(生成代码目录配置不规范会触发保守失效);⑤ Maven 版本过老(3.6 以下增量判定粗糙),升 3.9.x。

7.3 -o 离线模式报缺依赖,但我明明前几天还能构建?

通常是本地仓库里只有"上次构建实际用到的子集",而你切换了 JDK/profile/模块组合(如 -Pci 引入了新插件)。先去掉 -o 在线跑一次让它补齐,或针对性执行 mvn dependency:go-offline -Pci。另一种情况是依赖是 SNAPSHOT 且本地副本被清理过。离线模式是"预热之后的日常态",不是"任何时候都能开"。

7.4 跳过测试会不会埋雷?

本地 dev profile 跳过的是"执行",测试代码仍编译(用 -DskipTests 而不是 maven.test.skip),低级错误能挡住;真正的质量保障在 CI(-Pci 全跑)+ 提交前可选 mvn test -pl 改动模块。风险点在于开发者长期本地不跑测试,推到 CI 才红——补救方法是 IDE 装保存时自动跑相关单测,或 pre-commit hook 跑受影响模块测试。原则:跳过的是重复等待,不是测试责任,CI 门禁必须完整。

7.5 Gradle 是不是天生比 Maven 快,要不要迁移?

Gradle 的构建缓存(task 级增量、配置缓存、daemon 常驻)确实比 Maven 原生更激进,大型多模块项目全量构建通常快 2~4 倍。但迁移成本不低(DSL 重学、插件生态差异、CI/发布脚本全部重写),而本文五件套能让 Maven 拿到大部分收益。建议:新项目/构建是主要痛点的超大型 monorepo 可以评估 Gradle;存量 Maven 项目先把五个配置做满,30 秒的本地构建已经足够消除等待感,迁移收益可能还不如你想象的大。

7.6 怎么知道构建时间到底花在哪个模块/插件上?

  • 粗看:构建结尾的 Reactor Summary 每模块耗时,找 TOP 5
  • 细看:mvn -X package(debug 输出插件执行时间)或用 mvn com.github.jk1:maven-build-time-parser:parse / Gradle 风格的时间轴插件
  • 对比:固定一台机器、同一命令(冷构建先 clean 且清空 Build Cache,热构建保留缓存),分别记录——提速优化必须先量化基线,否则不知道哪个动作真的有效

八、总结

提速配置速查卡

.mvn/maven.config:     -T 1C
.mvn/extensions.xml:   maven-build-cache-extension
根 pom profile:        dev 默认(skipTests/checkstyle/spotbugs/jacoco.skip=true)
                        ci 显式 -Pci 全开
compiler plugin:       useIncrementalCompilation=true, staleMillis=100
日常命令:              mvn package(不加 clean,依赖预热后加 -o)
SNAPSHOT:              updatePolicy=never,需要时 -U
镜像:                  spring-boot layers 分层,依赖层前置
CI 缓存目录:           ~/.m2/repository、Build Cache、Docker layer、Paketo /cache

一句话

Maven 构建慢的 5 分钟里,真正用于编译的可能不到 1 分钟,其余全是串行等待、全量重编、质量检查、网络检查和重复打包——五个提速手段恰好各打一块:-T 1C 让没有依赖关系的模块并行构建(reactor 只保证依赖链串行,同层模块随便并行),收益最大且零风险;增量编译配合"日常绝不 clean"把改一行重编全部变成只编受影响类(clean 是排查消毒剂不是日常操作);dev/ci 双 profile 让本地默认跳过测试执行和静态检查、CI 显式 -Pci 全量门禁,把"靠人记参数"变成"默认值即最优";依赖预热后用 -o 离线模式 + SNAPSHOT updatePolicy=never 彻底消除每次构建的网络抖动;Build Cache Extension 让未改动模块整模块 CACHED 跳过、Spring Boot 分层 JAR 让 200MB 依赖层在镜像构建中长期命中缓存。落地顺序按投入产出比来:双 profile 和 -T 1C 当天就能把 5 分钟打到 40 秒拿走 80% 收益,Build Cache 和镜像分层再压到 30 秒以内。最后两条纪律:任何优化先量化基线(Reactor Summary + 冷/热构建对比),本地跳测试可以但 CI 质量门禁一个都不能少——提速是消除重复等待,不是降低交付质量。

给团队的建议

项建议
立刻做dev/ci profile、.mvn/maven.config 提交到仓库、改掉 clean package 习惯
本周做compiler 插件版本统一 3.13+、依赖预热文档、staleMillis
评估做Build Cache 小项目试点、Dockerfile 分层、CI 缓存目录持久化
不要做本地盲目 -T 4C、CI 关测试、长期不 -U 导致 SNAPSHOT 陈旧
度量记录冷/热构建基线,每次优化前后对比耗时

互动话题:你们项目的 Maven 构建要几分钟?最慢的模块或插件是什么?试过 Gradle 迁移吗?评论区聊聊你的提速数据。


参考资料


标题:Maven 构建提速:这 5 个配置让打包时间从 5 分钟降到 30 秒
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/09/20/1789823644663.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消