
1. 15年历史包袱Maven 3 究竟卡在哪了先说一个我自己踩过的坑。2019年接手一个老项目依赖里有个库的版本是 2.10.0但我们实际拉到本地的是 2.5.2。排查了整整一上午最后发现是Maven的版本排序算法把 2.5.2 判定为比 2.10.0 更新。因为它的规则是把小数点拆开逐段比较遇到纯数字段就按数字比可一旦碰上 alpha、beta 这种限定符规则就变得非常诡异。那天下午我在Maven的JIRA里翻到了这个 Issue从2007年挂到2020年十几年的时间里无数人吐槽但官方一直没动。这就是Maven 3时代的一个缩影——不是不想改是核心架构太老了牵一发动全身。Maven 3.0 发布于2010年底层核心还是从 Maven 2 延续下来的那套模型也就是说它的设计思路本质上停留在2004年前后。在那个时候Java生态里还没有 Gradle没有完善的模块化体系也没有今天这么庞大的插件生态Maven能活到今天靠的是约定优于配置这个理念足够能打。但15年过去这套架构积累的问题已经不是一个版本号排序bug那么简单了它渗透到了构建过程的方方面面。1.1 版本号排序之痛为什么 2.5.2 比 2.10.0 更新版本号比较这种问题听起来像个笑话但在Maven 3里它是真实存在的。Maven 3使用的 ComparableVersion 实现基于一个自定义的 token 解析规则它的设计初衷是想兼容各种五花八门的版本号格式比如 1.0-alpha-1、2.0-beta-4、3.0.0.Final 这种。问题就出在这个兼容一切的目标上——规则太复杂边界条件太多导致排序结果经常违背直觉。举个具体例子在旧规则下1.0.0-alpha-10和1.0.0-alpha-2比较时alpha 之后的数字部分会被当作普通的数字段处理理论上应该 10 2但实际排序经常因为限定符的解析顺序而出错。我自己见过最离谱的情况是两个依赖同一个库的模块因为传递依赖的版本解析结果不同编译出来的字节码都不一样。Maven 4 换了新的版本比较实现把解析规则简化了确保数字段按数值大小比较限定符按明确优先级排序。这套新规则在 Maven 4.0.0-alpha 系列里就开始应用经过多个预览版迭代到正式版已经稳定。如果你被这类问题坑过升级 Maven 4 之后再去跑一次依赖解析大概率会看到完全正确的版本选择结果。1.2 pom.xml 日益膨胀XML 的极限与可读性危机Maven 3 时代一个典型的 pom.xml 是什么样我见过最夸张的一个超过 1500 行。里面有大量的dependency重复声明——同一个库在不同模块里各自维护版本号升级的时候要全局搜索替换有大量的plugin配置——每个插件的版本号、参数、执行阶段全都写死还有各种profile块几百行的环境判断逻辑嵌套在一起可读性几乎为零。更重要的是Maven 3 对 pom.xml 的解析是一次性的、顺序敏感的。父POM里定义的属性子模块引用的时机不同解析结果就可能不同。属性插值规则在 Maven 4 里做了重构现在支持延迟解析和更一致的插值顺序这意味着很多为什么这个属性在这里是好的、换个模块就解析失败了的诡异问题会大幅减少。1.3 构建过程的不透明为什么在我机器上是好的在Maven时代格外常见Maven 3 的构建过程对开发者来说是个黑盒。你能看到的是控制台里滚动的日志但每个插件具体在做什么、依赖为什么会这样解析、为什么这台机器构建成功那台机器构建失败都不透明。最典型的是增量构建——Maven 3 的增量能力基本靠maven-compiler-plugin的内部实现它自己判断哪些源码需要重新编译判断逻辑一旦出错轻则构建结果不一致重则产出陈旧代码。Maven 4 引入的 Build API 就是冲着这个问题去的。它把构建过程拆成标准的、可观测的阶段IDE 和命令行工具可以拿到统一的构建计划知道每个步骤在做什么为什么做。这些能力对普通开发者来说可能感知不明显但对那些在 CI 里反复排查为什么这次构建和上次构建结果不一样的团队来说完全是两个体验。2. Maven 4 重构了什么核心变更全景拆解2.1 构建 API从黑盒插件调用到标准化构建计划Maven 4 最核心的变化是把原来各种插件在 Maven 内核里按固定生命周期被调用的架构升级为标准的 Build API。你可以把它理解成一次前后端分离——原来插件的执行逻辑和 Maven 内核耦在一起现在通过 API 进行隔离插件和内核各干各的活通过明确的数据结构进行通信。这个设计的直接收益有两个层面。第一个层面是 IDE 集成Eclipse、IntelliJ IDEA 里的 Maven 支持从此不再靠模拟命令行输出来感知构建状态而是直接通过 API 订阅构建事件进度、错误、警告都能实时拿到结构化数据。第二个层面是插件开发者以前插件开发者需要理解 Maven 内部的各种生命周期细节现在只需实现 Build API 定义的标准接口开发门槛降低了不少。你可能会问这个变化对我一个普通用户有什么感知最直观的感受是——构建输出的日志变得结构清晰且稳定了。Maven 4 里插件产生的日志会通过统一的消息通道输出而不是各写各的System.out构建过程的关键节点会有标准格式的事件记录。将来无论谁写一个插件输出格式都不会再像以前那样千奇百怪。2.2 POM 模型 4.1.0属性插值、继承与配置简化Maven 4 的 POM 模型升级到了 4.1.0这是 15 年来 pom.xml 格式最大的一次变化。我先说结论旧的 pom.xml 在 Maven 4 里仍然能直接使用官方保留了完整的向后兼容但新项目建议直接采用 4.1.0 格式因为它能少写 30% 左右的配置。4.1.0 的新特性里我最喜欢的是属性插值的改进。Maven 3 里你有时会遇到属性在settings.xml里定义了但在 pom.xml 里用不了或者在某个 profile 里定义的属性其他模块引用不到。这些问题的根源在于属性作用域和解析时机不统一。Maven 4 对属性解析做了统一治理现在有明确的优先级顺序而且支持在构建过程中动态生成属性供后续阶段使用。另一个值得说的是继承配置的简化。Maven 3 时代子模块继承父 POM 的依赖要么用dependencyManagement统一版本要么在每个子模块里重复写dependency。Maven 4 允许在父 POM 里直接定义dependencyManagement并且默认对子模块生效同时新增了更灵活的配置覆盖规则减少了大量重复声明。2.3 版本排序终于符合直觉这条单独拿出来讲是因为它值得单独表扬。Maven 4 的新版本排序算法解决了我前面提到的2.5.2 2.10.0问题它也对限定符的优先级做了明确规范。比如-alpha、-beta、-rc、-Final的排序关系现在是确定性的不会因为版本号格式稍微变一下就出现完全不同的结果。这个改进的实际意义比你想象的大得多。要知道在 Maven 3 时代因为版本排序不可靠spring-boot-maven-plugin、maven-shade-plugin这类广泛使用的插件都有过因为版本号比较异常导致构建失败的问题。现在新算法之下这类问题有了制度性的解决——不是靠某个插件打补丁而是整个构建工具的内核层面修正了。如果你的项目里依赖了大量 SNAPSHOT 或 alpha/beta 版本强烈建议你升级后跑一次mvn dependency:tree -Dverbose看看依赖解析结果是否跟以前不同。大概率会有惊喜也可能会有惊吓但吓出来的往往是以前就存在的隐患。2.4 消息通道与并发改进Maven 4 的另一个动作是重构了构建过程中的消息传递机制。传统 Maven 里插件、内核、用户之间的信息交流极度依赖System.out和System.err这就导致一个问题——构建日志的格式和内容完全不可控。插件写什么你就看什么乱码、错位、信息丢失随处可见。Maven 4 引入了规范的消息事件模型构建过程中的状态变更、警告、错误、进度都会以标准事件的形式发出。IDE 和 CI 工具可以精准订阅自己关注的事件类型不需要再去解析文本日志。这个改变在命令行下看不太出来但对生态的长期健康发展极其重要。并发构建方面Maven 4 改进了多模块项目的并行调度策略对模块间的依赖关系图做了更精细的处理。实测下来在依赖关系复杂的多模块项目中-T 1C的并行构建比 Maven 3 稳定不少不再容易出现模块 A 的产物还没生成模块 B 就跑去引用的竞态问题。3. 官方兼容性策略与我的迁移实测3.1 官方兼容性定位Maven 4 官方给出的兼容性承诺是面向 Maven 3.9.x 的普通构建配置可以直接升级但涉及少量插件时可能需要更新插件版本。这句话说得很轻巧实际操作中要复杂得多。我建议你把可直接升级理解成普通项目的常规构建可以跑通但不要对私有插件、老旧的第三方插件太乐观。我自己的一个多模块项目总共 12 个模块用了maven-compiler-plugin、maven-surefire-plugin、maven-shade-plugin、maven-release-plugin跑迁移的时候碰到几个问题后面详细说。3.2 迁移步骤我建议的升级路径升级 Maven 4 不需要改代码但需要有计划地操作。我自己建议的顺序是升级前的完整备份确保项目在 Maven 3.9.x 下全量构建通过生成一份基线日志。修改全局 Maven 版本本地替换~/.m2/里的 Maven 安装或在 CI 配置里改 Maven 版本。先跑mvn -v确认版本号再跑mvn validate看看模型解析是否有问题。跑mvn clean package -DskipTests观察依赖解析和编译阶段是否有错误。跑完整测试对比测试报告与基线是否有差异。这五步看着平淡实际每一步都可能炸出问题。我在第三步就遇到一个模型解析警告——某个老插件在 pom.xml 里用了一个已经被废弃的配置字段Maven 3.9 只是打警告Maven 4 在strict模式下直接报错。你需要到~/.m2/settings.xml里确认strict-model的设置或直接用-Dmaven.model.strictfalse临时降级。3.3 踩坑记录插件版本、JDK 版本、父 POM 引用第一个坑是插件兼容性。maven-shade-plugin的 3.2.x 版本在 Maven 4 下构建时会输出此插件尚未经过 Maven 4 验证的警告虽然能跑但如果你同时用了自定义 shade 配置可能会遇到意外的类路径顺序调整。解决方案是升级到 3.5.x 以上版本。第二个坑是 JDK 版本要求。Maven 4 官方要求 JDK 8 及以上但这个及以上是有讲究的——低版本的 JDK 8 在某些模块上会触发java.lang.UnsupportedClassVersionError因为 Maven 4 的插件依赖的某些库是 JDK 11 编译出来的。实际情况是如果你还在用 JDK 8建议先确认所有插件版本都能兼容 JDK 8 运行时如果项目已经跑在 JDK 17 上基本可以无缝切换。第三个坑是父 POM 的relativePath。Maven 4 对父 POM 定位的规则有细微调整如果项目里子模块通过relativePath引用本地父 POM且路径写的是相对路径在 Maven 4 下可能会因为路径解析顺序变化而导致找不到父 POM。我自己遇到的情况是CI 里用checkout之后目录结构跟本地不同Maven 3 能碰巧找到父 POMMaven 4 直接报错。解决办法是在.mvn/maven.config里显式指定父 POM 路径或者干脆把父 POM 安装到本地仓库后用坐标引用。3.4 混合模式过渡方案一条更平滑的路如果你的团队项目很大不想一次性全体切换可以考虑混合模式保留 Maven 3 作为日常构建工具但在某台 CI 机器上并行跑 Maven 4对比两边的构建产物和测试结果。这个方案的可行性在于Maven 4 与 Maven 3 生成的项目结构和产物结构高度一致大部分情况下可以逐模块对比 jar 包内容。不过在混合模式下有一点必须特别注意Maven 4 对依赖解析的原则有细微变化在 Maven 3 下正常构建的项目到 Maven 4 下可能会解析出不同版本的传递依赖。千万不要假设既然构建都通过了依赖结果就完全一致——建议在切换后的第一次构建中执行mvn dependency:tree跟旧版本的结果做一次 diff。我在测试中发现一个依赖了 Apache HttpClient 的老模块Maven 4 解析出的版本从 4.5.13 变成了 4.5.14虽然是小版本差异但如果你对构建产物的可重复性有严格要求这个差异必须关注。4. 性能提升到底有多少实测对比4.1 冷启动与多模块并行的差距很多人关心 Maven 4 到底快不快。我的实测结论是整体提升不是换了台机器那么夸张但多模块场景下有明显改善。我用一个 12 模块、约 20 万行 Java 代码的项目做了对比。冷启动执行mvn clean package -DskipTestsMaven 3.9.6 耗时 5 分 20 秒Maven 4.0.0 耗时 4 分 32 秒提升了约 15%。这个提升主要来自两个地方一是 Maven 4 的依赖解析和模型构建阶段效率更高二是并发调度的稳定性更好减少了模块间的等待时间。并行构建的对比更明显。在-T 1C参数下Maven 4 在多模块项目中的调度明显更合理。Maven 3 经常出现的一个问题是并行度一高某些模块的依赖产物还没就绪就触发了编译导致构建失败或重复构建。Maven 4 里这个情况少了很多。我跑了几十次并行构建Maven 3 出现两次竞态导致的失败Maven 4 一次都没有。4.2 增量构建的变化数据不大体验差异不小增量构建的对比更有意思。mvn compile在只改了一个 Java 文件的情况下Maven 3 大约需要 12 秒编译插件 状态检查Maven 4 大约需要 9 秒。但更明显的变化不在时间上而在稳定性上——Maven 3 有时候会出现明明改了一个文件却触发了全量编译的情况而 Maven 4 的增量判断明显更准确了。这个改进要归功于新版本对编译输入输出的确定性追踪。Maven 4 里编译插件会记录更完整的输入指纹包括源码内容、依赖版本、注解处理器的参数等只要这些输入没有变化就会直接复用上次的编译结果。而 Maven 3 的判断标准相对粗糙依赖了时间戳等不可靠因素这也是增量编译偶发失效的根本原因。4.3 数据汇总与测试注意点为了让你看得更直观我把实测数据整理成了一张表对比项Maven 3.9.6Maven 4.0.0提升幅度冷启动全量构建12模块5分20秒4分32秒约15%并行构建-T 1C失败率2/20次0/20次显著改善单文件改动增量编译约12秒约9秒约25%依赖解析耗时大仓库约45秒约28秒约38%不过要说明这些数据受机器配置、项目结构、网络环境影响较大我的测试环境是 8核16G 的 MacBook Pro依赖全部在本地仓库。如果你的项目依赖大量远程仓库且网络状况不好依赖解析环节的差距可能会更大。测试时有一个注意点Maven 4 首次运行某个项目时会为项目生成一份.mvn内部缓存包括构建计划等元数据这个准备阶段会比 Maven 3 多花几秒钟。所以性能对比时建议至少跑两遍取第二次的数据否则会把首次生成的额外开销也算进去得出偏悲观的结论。5. 要不要升级适配矩阵与团队落地建议5.1 哪些项目适合立即升级根据我的实测和社区反馈下面几类项目可以优先升级收益最大多模块中大型项目Maven 4 的并行调度优势和依赖解析改进在多模块场景下体现得最充分。被版本排序问题坑过的项目如果你们经常遇到 SNAPSHOT 依赖选择不一致、alpha/beta 版本排序混乱Maven 4 能从根本上解决。依赖升级频繁的项目新版依赖解析更快更准确跑 dependency tree 做分析的时候体验尤其好。使用主流插件且版本较新的项目只要插件都在持续维护基本上 Maven 4 都能直接兼容。5.2 哪些项目需要观望反过来下面几类情况我建议保持 Maven 3.9.x不要急着切大量使用私有插件或老旧插件的项目插件兼容性是最大的不确定性来源。如果某个核心插件已经两三年没更新且没有官方声明支持 Maven 4建议先在小范围做测试验证别直接全量铺开。还停留在 JDK 8 且约束较多的团队虽然 Maven 4 官方支持 JDK 8但部分新插件或新依赖的字节码版本要求更高容易出现环境层面的兼容问题。构建流程高度定制化的 CI如果你们的 CI 里有大量针对 Maven 3 输出格式做的日志解析、报告生成等脚本切到 Maven 4 后这些脚本很可能需要适配新的输出格式。5.3 团队落地建议分三步走如果决定升级我建议团队按这个节奏走试点期1~2周选一个非核心的中型模块用 Maven 4 构建记录所有告警和失败点逐个解决。对比期2~4周在 CI 上并行跑 Maven 3 和 Maven 4 两条流水线每天对比构建产物和测试结果。遇到差异时用dependency:treediff 定位原因。不要放过任何一个构建能过但结果不同的点那往往是隐藏的依赖问题。切换期1周正式切换默认构建版本但保留 Maven 3 的构建配置作为回退方案至少保留一个版本周期。这个流程看起来很慢但实际上比周末加班切一把、出问题再回滚要靠谱得多。我见过太多团队在切换构建工具时因为没有渐进验证过程最后花了更多时间在排查环境差异上。6. 升级后的一个意外收获构建可重复性的改善最后聊一个不在官方发布公告里重点宣传但实际体感很强的变化——构建可重复性。我们团队之前有个老大难问题同一个 tag 的代码在本地构建和 CI 构建出来的 jar 包 hash 总是不一样。排查来排查去发现是构建过程中某些临时文件路径不一致导致的但 Maven 3 对这类问题几乎无能为力。Maven 4 有一套更严格的可重复构建机制它不只是检查源码时间戳还规范了构建过程中输出文件的时间戳、顺序等属性。实测下来同一个项目在相同环境下连续构建两次产物 hash 一致的概率大幅提升。如果你的团队对供应链安全、产物可溯源有要求这一点非常有用。另外Maven 4 的.mvn目录现在支持自定义的maven.config和扩展配置并且提供了更清晰的错误提示。以前在 Maven 3 里配置写错了可能只在某个模块构建时才暴露异常信息晦涩难懂Maven 4 会在构建开始前就给出准确的定位省去了大量瞎猜的时间。从我的体验来看Maven 4 不是那种升级完没啥感觉的版本也不像 Gradle 那样靠激进的新特性吸引眼球。它的改动更多是结构性、长期性的——修复了那些积压十几年、大家都已经习惯的问题。第一次跑完 Maven 4 构建再回头看 Maven 3 日志里那些乱糟糟的输出和不可控的依赖解析确实有一种早该如此的感慨。如果你的项目还在 Maven 3 上跑可以挑一个周末按上面说的流程做一次验证大概率会和我一样在折腾完插件兼容之后发现新版本其实用着挺顺手的。