Maven 与 Gradle 选型迁移:用构建证据决定,而不是重写冲动
“Gradle 更快,所以把 Maven 换掉”与“团队只会 Maven,所以永远不迁”都不是架构决策。构建工具不是一条命令,而是项目模型、插件、依赖解析、JDK、产物元数据、缓存和发布流程共同形成的可执行契约。迁移真正昂贵的部分,也不是把 XML 改成 Kotlin DSL,而是证明新旧构建在团队关心的行为上等价。
可靠的迁移顺序应当是:先判断问题是否真的来自构建工具,再建立旧构建证据基线;随后用并行构建验证新模型,逐项比较依赖、测试、产物和发布元数据;只有在停止条件全部解除后,才切换权威 Wrapper。这样,即使迁移失败,团队也能恢复旧入口,而不是在两个半成品构建之间长期摇摆。
迁移先从一条可复现的旧构建开始
Maven 与 Gradle 的差异横跨项目模型、生命周期与任务图、插件生态、JDK Toolchain、企业 parent/BOM、约定插件、缓存和 CI 证据。真正需要迁移的是这些工程行为,不是把 POM 的 XML 改写成另一种 DSL。Spring、Android 等框架专用构建逻辑,以及 Nexus、Artifactory、签名和发布审批,也不能靠通用转换命令自动继承。
先确认同目录的 Maven 与 Gradle 工程手册都能用 Wrapper 跑通,再把迁移放进可回滚分支,并为试发布分配非生产坐标。旧构建若无法从干净缓存稳定执行,就没有资格充当新构建的比较基线。
开始前记录:
JDK 与 Toolchain 基线
当前权威 Wrapper 及校验值
父 POM、BOM、约定插件和内部插件
仓库、镜像、代理、CA 与凭证来源
所有模块、测试类型、生成代码和发布物
CI 阶段、缓存边界、签名与发布元数据迁移负责人需要有权限读取 effective POM、Gradle task/dependency 报告和 CI 日志,但仓库凭证仍应从用户级配置或 CI secret 注入。Maven Wrapper 与 Gradle Wrapper 的版本、下载地址和校验值也要随证据保存;升级 Wrapper、JDK 或插件应与构建迁移拆开,否则差异无法归因。遇到版本兼容问题时,分别核对 Maven 发布历史 和目标 Gradle 兼容矩阵,不要从另一台机器的成功结果反推当前组合受支持。
先判断是否应该迁移
两种模型解决问题的方式不同
Maven 以声明式项目模型为中心。POM 经过父子继承、profile、settings 和 Super POM 合并成为 effective model,packaging 把插件 goal 绑定到固定生命周期,Reactor 按模块关系执行。它的优势是约定清晰、企业 parent/BOM 容易集中治理、陌生项目可预测;代价是复杂定制往往需要插件、profile 或额外模块表达。
Gradle 以可编程构建模型和任务图为中心。Settings 发现项目,DSL 配置 Project 和 Task,初始化、配置、执行三个阶段共同形成任务图;输入输出声明又决定增量构建和缓存是否可信。它适合复杂任务编排、多项目共享 build logic、细粒度性能优化和需要变体模型的生态,但自由度会把团队带入“每个仓库一套框架”的治理成本。
因此,选型问题不是 XML 与 Kotlin 哪个好看,而是团队需要更强约定还是更强建模能力。若现有问题只是仓库慢、JDK 漂移、插件版本失控或 CI 缓存配置错误,先修治理问题,换工具不会自动消除它们。
什么情况下不迁
出现以下任一情况,应停止立项或延后迁移:
现有构建稳定,构建时长并未影响交付,迁移收益只有“语法更现代”。核心插件、代码生成器、签名或发布流程在目标工具中没有等价实现。团队无法说明所有产物、测试、profile、variant 和发布坐标,也没有代表性验收集。
构建依赖未固定,旧构建本身不能从干净缓存复现。发布窗口不允许双轨运行,失败后又无法恢复旧 Wrapper。迁移同时夹带 JDK、框架、依赖大版本和仓库切换,无法归因差异。
架构师应把“不迁”写成有期限的决策记录:当前瓶颈、证据、重评条件和 owner。它不是保守口号,而是避免用重写掩盖未知系统行为。
选型决策面
| 决策面 | Maven 更自然的场景 | Gradle 更自然的场景 | 必须拿出的证据 |
|---|---|---|---|
| 项目模型 | 标准 JVM 模块、统一 parent/BOM | 复杂多项目、复合构建、定制变体 | 模块图、继承或 build logic 来源 |
| 执行模型 | 固定生命周期和成熟插件绑定 | 自定义任务图、细粒度输入输出 | 实际 phase/goal 或 task graph |
| 性能 | 可接受的顺序构建、依赖稳定 | 增量、并行、Build Cache 有收益 | 冷/热缓存多次测量与命中原因 |
| 生态 | 企业 Maven 插件与发布规范成熟 | Gradle 插件或 Android 生态主导 | 插件兼容矩阵和维护状态 |
| JDK | Toolchains + 插件约束 | Java Toolchains + Wrapper/Daemon 分层 | 编译、测试、运行 JVM 报告 |
| 治理 | parent、BOM、Enforcer | convention plugin、version catalog、verification | 干净仓库可恢复基线 |
| 团队能力 | 依赖约定优先、定制较少 | 能维护 DSL、插件和缓存正确性 | owner、评审人、排障演练 |
表格只能帮助发问,不能直接得出结论。尤其不要引用供应商的单次性能数字代替本仓库数据。构建速度必须同时测冷缓存、热依赖缓存、代码无变化、单模块变化和公共模块变化,并记录机器、并发度、远端缓存和网络条件。
建立迁移前证据基线
固定输入
把旧 Wrapper、JDK、Toolchain、仓库镜像、依赖锁定或约束、插件版本、环境变量名称和执行参数记录为机器可读清单。凭证只记录注入位置和权限范围,不记录值。
Maven 基线可从以下入口采集:
./mvnw --version
./mvnw help:effective-pom -Doutput=build-evidence/effective-pom.xml
./mvnw help:effective-settings -Doutput=build-evidence/effective-settings.xml
./mvnw dependency:tree -DoutputFile=build-evidence/maven-dependencies.txt
./mvnw test packageGradle 基线可采集:
./gradlew --version
./gradlew projects tasks --all > build-evidence/gradle-model.txt
./gradlew dependencies > build-evidence/gradle-dependencies.txt
./gradlew test assemble --no-build-cache --rerun-tasks
./gradlew outgoingVariants > build-evidence/gradle-variants.txteffective-settings.xml 可能包含敏感服务器信息,进入构建证据包前必须脱敏并限制访问。公开仓库只保留结构摘要或自动检查结果。
固定输出
至少记录四类输出:
产物:文件名、类型、大小、SHA-256、归档文件清单、MANIFEST 和可复现性状态。依赖:group/name/version、冲突选择、scope/configuration、可选依赖、排除项和仓库来源。测试:单元、集成、契约、代码生成前后测试数量、跳过项和报告路径。
发布:坐标、POM、Gradle Module Metadata、sources/javadoc、签名、校验和与仓库路径。
二进制哈希不同不必然代表失败,时间戳、归档顺序和生成器版本都可能改变字节。先解包比较内容,再决定是否要求 bit-for-bit 等价。对于可执行 JAR、原生镜像和代码生成产物,还要运行最小功能验证。
Maven 迁移到 Gradle
第一步:只导入模型,不删除 Maven
在迁移分支保留 pom.xml、.mvn/ 和 mvnw*。使用受控 Gradle 生成 Wrapper,再按 Gradle 的 Maven 迁移步骤用初始化入口读取 POM:
gradle init --type pom
./gradlew --version
./gradlew projects生成结果必须人工审查。parent、profile、插件 extension、annotation processor、代码生成、integration-test、shade、assembly 和发布配置很容易需要手工建模。不要因为 ./gradlew build 退出码为 0 就认为迁移完成。
第二步:重建依赖与生命周期语义
把 Maven 的 compile/runtime/test scope、optional、exclusion、BOM 和 plugin dependency 映射到 Gradle configuration、platform、constraint 与 task。逐项比较:
./mvnw dependency:tree
./gradlew dependencies
./gradlew dependencyInsight --dependency <占位依赖名>Maven verify 中的集成测试、质量插件和生成步骤,不一定自动落入 Gradle build。新构建必须显式建立任务依赖,并用 ./gradlew <task> --dry-run 或 task 报告证明顺序。
第三步:先求正确,再开缓存
首轮用 --no-build-cache --rerun-tasks 建立无缓存结果。只有任务输入、输出、环境依赖和副作用已声明,才分别评估增量构建、Build Cache 和 Configuration Cache。若一开缓存产物就变化,问题是任务建模不完整,不能用清缓存作为长期修复。
第四步:并行 CI
新旧构建在同一提交、同一 JDK、同一依赖镜像上运行,但发布到隔离坐标。比较测试报告、依赖清单、归档内容和元数据。至少覆盖一次干净缓存、一次热缓存和一次公共模块变化;只跑“没有改代码”的热构建会高估收益。
Gradle 迁移到 Maven
反向迁移通常发生在团队希望减少定制、统一企业 parent/BOM,或目标平台只接受 Maven 生命周期时。它不是把 Kotlin DSL 翻译成 XML,而是识别哪些 Gradle 能力需要保留、改写或舍弃。
第一步:冻结 Gradle 模型
保留 settings.gradle*、build.gradle*、buildSrc、included builds、version catalog 和 Wrapper。导出 project、task、dependency、variant 和 publication 证据,特别标记自定义 task 的输入输出及其调用外部程序的方式。
第二步:建立 Maven 模块与企业模型
先创建聚合 POM 与最小模块,让 mvn test package 只覆盖标准 Java/Kotlin 编译和测试。再依次接入 BOM、pluginManagement、Toolchains、代码生成、集成测试和发布。Gradle convention plugin 中的规则应转成 parent、BOM、Enforcer 或可维护的 Maven 插件配置,不能散落复制到每个模块。
第三步:处理无法直接映射的能力
Gradle variant、lazy provider、自定义 task graph、composite build 和配置缓存没有一一对应的 Maven XML。可选方案是拆成独立模块、绑定成熟插件、把通用逻辑做成 Maven 插件,或明确取消能力。每个取舍都要记录对产物、性能和维护人的影响。
第四步:恢复发布等价性
对比 maven-publish 生成的 POM、Gradle Module Metadata 与 Maven 新构建的 POM。Gradle 的发布元数据说明解释了 GMM 与 POM 并存时各自承载的依赖信息。若下游 Gradle 消费者依赖 variant 或 GMM,仅发布 POM 可能改变依赖选择。必须在代表性下游项目中解析并运行,而不是只检查仓库里“有文件”。
切换权威入口
迁移期间允许两个 Wrapper 存在,但只能有一个发布权威。达到以下条件后,才把 CI 必需检查、开发文档和 IDE 委托构建切到新 Wrapper:
新旧构建在代表性提交上连续通过,测试集合和跳过规则一致。依赖差异均有批准结论,没有未知仓库来源或动态版本。产物内容、入口、运行行为和发布元数据满足验收标准。
冷缓存与热缓存性能达到事先定义的收益阈值,且没有牺牲正确性。代理、CA、私有仓库和最小权限服务账号在 CI 可重建。至少两名维护者能解释新模型、定位失败并执行回滚。
切换提交应只修改权威入口、CI 门禁和文档,不同时升级框架或业务依赖。旧构建进入短期只读冻结期,禁止继续增加新能力。
停止条件与回滚
发现以下现象立即停止迁移:依赖来源无法解释;测试数量减少;生成代码或资源缺失;POM、签名或坐标不兼容;缓存命中改变产物;私有凭证只能硬编码;关键插件无人维护;代表性下游无法消费。
回滚不是“把新文件删掉”。切换前应保存旧 Wrapper、旧 CI job 和旧发布坐标;切换后若失败:
1. 禁止新构建继续发布。
2. 将必需检查和发布 job 恢复到旧 Wrapper。
3. 从已审查提交恢复旧构建文件与版本基线。
4. 清理新工具独有缓存,避免证据混用。
5. 用旧构建重跑测试、产物和下游消费验证。
6. 记录差异与触发条件,再决定修复还是终止迁移。Gradle 回滚到旧 Wrapper 时恢复 gradle-wrapper.properties 和 Wrapper JAR/脚本的受审查版本;Maven 同理恢复 .mvn/wrapper 与 mvnw*。不要在故障现场重新在线生成 Wrapper。
新构建通过,但测试变少
现象是退出码为 0,报告中的测试数量或集成测试阶段却下降。先比较旧构建的 phase/goal 与新构建 task,检查命名规则、source set、Failsafe/Surefire 或 Gradle Test Suite。修复任务绑定后,从干净目录重跑并比较报告,不接受“核心用例还在”的口头判断。
依赖版本不同
Maven 与 Gradle 的冲突选择、BOM/platform、optional 和 metadata 语义不同。先输出依赖树和 dependency insight,定位直接约束、传递依赖与仓库元数据;再用 constraint、platform、dependencyManagement 或 exclusion 显式表达。禁止手工复制整个依赖树成为永久固定清单。
本地更快,CI 更慢
本地可能命中长期缓存和 Daemon,CI 每次是冷环境。比较网络下载、配置时间、执行时间、缓存上传下载和解压成本;远端缓存条目小且命中率高时才有收益。修复后用相同 runner 规格重复多次,报告中位数与高分位,不只截取最快一次。
发布物存在但下游失败
检查坐标、classifier、POM scope、GMM variant、签名和仓库路径。使用代表性 Maven 与 Gradle 下游项目从隔离仓库解析,并运行最小入口。只有服务端“上传成功”不能证明发布兼容。
迁移双轨会把依赖下载和缓存流量放大。Maven settings.xml 与 Gradle gradle.properties 的代理、mirror/repository 和 CA 信任必须分别配置,但凭证应由用户级或 CI secret 注入。不要把带账号的 URL、settings.xml 或 gradle.properties 提交到仓库。
并行构建使用只读依赖账号;试发布使用隔离 namespace 和最小写权限账号。构建日志、effective settings、Build Scan 或诊断归档可能泄露仓库地址、用户名和环境变量,上传前必须脱敏并设置保留期。远端缓存也要区分读写:普通分支默认只读,受信任主线才写入。
团队必须指定构建 owner、每种插件或 build logic 的维护人、升级窗口和回滚批准人。新项目模板只暴露 Wrapper 入口,IDE 委托给同一入口;CI 禁止调用全局 mvn 或 gradle。迁移完成后建立以下门禁:
Wrapper 文件与校验值受 CODEOWNERS 审查。JDK、插件和依赖升级拆分提交,并生成可比较证据。构建缓存有命中率、错误复用和清理责任人。
构建脚本变更必须说明任务图、产物或发布元数据影响。旧构建在冻结期结束后删除,并保留可恢复 tag 与迁移记录。
迁移把隐式行为变成静默缺失
旧构建中 profile、环境变量或插件默认值可能没有文档。新构建“更干净”时,这些行为会消失。判断标准不是构建成功,而是测试、生成文件、归档内容和下游行为均被证据覆盖。无法说明来源的隐式行为应先显式化,再迁移。
缓存让错误结果更稳定
Gradle task 漏报输入、Maven 插件读写工作区外文件,都会让缓存或增量判断失真。现象是干净构建正确、热构建错误。排查时关闭所有缓存并强制重跑,再逐层启用;只有输入输出和环境依赖可解释,才允许共享远端缓存。
企业 build logic 成为第二套平台
parent POM、BOM、Gradle convention plugin 和内部插件都可能形成团队平台。迁移若复制规则而不迁移 owner、版本和兼容策略,会得到大量分叉。应把公共逻辑独立版本化,先验证两个代表仓库,再扩大覆盖;若每个仓库都要特判,说明目标抽象不成熟。
双轨阶段污染发布与缓存
两个构建若写同一仓库坐标或共享可写缓存,结果无法归因。双轨必须使用隔离坐标、独立工作目录和明确缓存 namespace;未经批准的分支不得写正式发布仓库。发现同版本产物内容不同,立即冻结发布并清理污染条目。
团队只会运行,不会解释
迁移完成后若只有作者能维护,新工具就是单点故障。验收必须包含交叉演练:另一名维护者从干净机器恢复构建、解释依赖选择、定位一次故意制造的失败并回滚 Wrapper。演练失败时,不得结束冻结期。
迁移收益有本仓库证据,不是供应商宣传或个人偏好。已记录旧 Wrapper、JDK、Toolchain、依赖、插件、模块、测试和发布基线。自动转换结果经过人工审查,没有把“能运行”当成语义等价。
新旧构建在同一提交和同一外部输入下并行验证。产物、依赖、测试、发布元数据和代表性下游消费均已比较。冷缓存、热缓存和典型代码变化均有性能数据。
代理、CA、仓库与凭证不进入仓库或公开证据包。停止条件、隔离发布坐标、旧 Wrapper 恢复步骤已经演练。至少两名维护者能够排障和回滚。
不迁或终止迁移时,也留下了重评条件与责任人。
