Maven 多模块实战:根 POM 版本号不生效?揭秘依赖版本管理失控的乱象
栏目老炮踩坑录 · 翻车现场系列项目基础档案 企业真实项目编号ARC-F03素材根pom.xml、子模块pom.xml 欢迎阅读个人主页知守观我的专栏老炮踩坑录当前内容Maven 版本管理的“潜规则”文章目录现场机制子模块一旦硬编码根 POM 就管不着怎么演变成这样的付出的代价怎么改写在后面我一直以为多模块工程的根 POM 是版本号的真正源头。properties 元素里定义dependencyManagement 元素里引用子模块声明依赖时不带版本号一切都是由根 POM 说了算。教科书上是这么写的。直到我打开基础档案这个项目把三个 pom.xml 文件摆在一起对了一遍发现根 POM 里那些版本号一部分根本没生效一部分跟子模块实际用的对不上。最过分的一个连 Maven 内置属性都被改了名。现场先看根 POM 的 propertiesproperties java.version1.8/java.version nacos.version0.2.12/nacos.version easyexcel.version3.1.1/easyexcel.version hutool.version5.7.7/hutool.version org.mapstruct.version1.4.1.Final/org.mapstruct.version project.version0.0.4-SNAPSHOT/project.version api.version0.0.2-SNAPSHOT/api.version /propertieseasyexcel 3.1.1mapstruct 1.4.1.Final。dependencyManagement 里规规矩矩地用${easyexcel.version}和${org.mapstruct.version}引用写法挑不出毛病。再看子模块。公共模块的依赖声明dependency groupIdorg.mapstruct/groupId artifactIdmapstruct/artifactId /dependency dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.2.1/version /dependency同一个文件里紧挨着的两个依赖。mapstruct 不写版本走根 POM 管理拿到 1.4.1.Final。easyexcel 硬编码 3.2.1根 POM 里那个 3.1.1 当没看见。web模块更彻底mapstruct 直接顶到 1.5.3.Final而且一个文件里写了三处dependency groupIdorg.mapstruct/groupId artifactIdmapstruct/artifactId version1.5.3.Final/version /dependency dependency groupIdorg.mapstruct/groupId artifactIdmapstruct-processor/artifactId version1.5.3.Final/version /dependencymaven-compiler-plugin 的 annotationProcessorPaths 里还有一处 1.5.3.Finalplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.8.1/version configuration source1.8/source target1.8/target annotationProcessorPaths path groupIdorg.mapstruct/groupId artifactIdmapstruct-processor/artifactId version1.5.3.Final/version /path /annotationProcessorPaths /configuration /plugin要升 mapstruct要改三个地方漏一处就会出现编译期和运行期版本不一致的问题。三组对不上的数字列成表第三组值得单独说。project.version是 Maven 内置属性正常情况下等于 POM 自己的 version 标签。这个项目根 POM 的 version 是 0.0.1-SNAPSHOTproperties 里却又定义了一遍写成 0.0.4-SNAPSHOT。有个细节我没完全确认项目里把 Maven 的内置属性project.version重定义成了0.0.4-SNAPSHOT。我在本地跑是能用但不知道在其他更低版本的 Maven 或者 IDEA 的不同版本下这种覆盖内置属性的黑魔法会不会导致dependencyManagement解析直接失效有没有遇到过类似问题的兄弟评论区指点一下。dependencyManagement 里管理 api 模块和公共模块用的就是这个${project.version}。这行配置到底解析成 0.0.1 还是 0.0.4子模块继承之后行为如何说实话我第一眼没看懂得跑下命令mvn help:evaluate才能验证。一个需要做实验才能读懂的版本号本身就是 “写了没人看” 的最典型例子。顺带一提根 POM 的 description 还是脚手架生成时的 “Demo project for Spring Boot”。建项目那天写完就没人再看过一眼。机制子模块一旦硬编码根 POM 就管不着Maven 的版本解析规则其实很直白依赖声明里显式写了 version就用显式的dependencyManagement 只对没写版本的依赖生效。也就是说根 POM 的 dependencyManagement 管不到不听话的子模块。easyexcel 那行 3.1.1 写上去的那一刻起它管理的是未来可能出现的新依赖对已存在的硬编码没有任何约束力。mapstruct 的情况更微妙。common 不写版本走管理1.4.1.Finalweb 硬编码1.5.3.Final。web 依赖 common整个应用最终打包时 mapstruct 是 1.5.3——但 common 模块单独构建、单独发布的时候它编译期用的是 1.4.1。同一个工程两个模块两代 MapStruct 编译出来的转换实现运行时挤在同一个 classpath 里。MapStruct 1.4 和 1.5 之间恰好有不少行为差异比如对 Lombok 的配合、对嵌套属性的解析。平时大概率相安无事一旦哪天报出诡异的 NoSuchMethodError 或者转换结果不对劲排查的人要绕很大一圈才会想到两个模块的 MapStruct 版本可能不一样。根 POM 里那行 1.4.1.Final在 common 身上是生效的在 web 身上等于没写。同一个属性一半管用一半不管用比完全不管用更迷惑人。怎么演变成这样的我不在现场只能从痕迹倒推。easyexcel 3.2.1 发布于 2022 年修了一批 3.1.x 的读写 bug。大概率是某个子模块在开发中撞上了 3.1.1 的 bug一急就硬编码了新版本想着回头再统一收回去。这一回头就是永远。破窗就是这么开始的。第一个硬编码出现之后后来的人翻到这段 pom看到 easyexcel 写了版本号照着抄就是了。谁也不会去想为什么这个写了那个没写。common 里 iaas-client 声明了两次第二次旁边还留着一段 poi 的注释块残骸——复制粘贴的痕迹连删都懒得删干净。这种细节说明这个 pom 文件长期处于能用就行的维护状态。没人对账。mvn versions:display-dependency-updates这类命令如果跑过一次三组不一致当场就能看出来。它没被跑过或者跑了没人看结果。付出的代价三笔账。第一笔安全修复会漏。easyexcel 历史版本出过漏洞通告如果哪天安全团队发通知全公司 easyexcel 升到 x.y.z运维同学改根 POM 的 properties改完发现线上版本没变——因为实际生效的是子模块里硬编码的 3.2.1。漏升一次通报里就有这个项目一个。第二笔混版本运行时风险。common 用 1.4.1 编译、web 用 1.5.3 编译这件事前面说了。这类问题不定期发作发作时排查成本极高因为没人会第一时间怀疑同一工程里 MapStruct 有两个版本。第三笔升级评估数不清家底。要评估这个项目能不能升级、有哪些依赖要动标准动作是盘点版本清单。拿根 POM 看是一个版本实际运行是另一个版本评估报告从第一行开始就是错的。怎么改三步从止血到根治。第一步把硬编码全部揪出来。全仓库搜一遍依赖声明里带version的行逐个判断该走管理的删掉版本号确需覆盖的在根 POM properties 里单独定义属性并注明原因。# 粗筛找出所有依赖声明里硬编码版本的位置 grep -rn --includepom.xml -A2 artifactId . | grep -B1 version第二步用 enforcer 插件把规矩立起来。requireUpperBoundDeps和banDuplicatePomDependencyVersions这两条规则能挡住大部分版本混乱plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-enforcer-plugin/artifactId executions execution idenforce-version-rules/id goalsgoalenforce/goal/goals configuration rules banDuplicatePomDependencyVersions/ requireUpperBoundDeps/ /rules /configuration /execution /executions /plugin规矩立起来之后再有人想在子模块里硬编码版本构建直接失败理由必须写在根 POM 里。第三步CI 加版本对账。定期跑mvn versions:display-property-updates和dependency:tree输出物里加一道检查properties 里定义的属性必须有至少一个地方在引用。没人引用的属性和没人看的版本号一样都该清掉。至于那个被重定义的project.version我的建议是连根拔掉。子模块版本就该老老实实写在各自 POM 的 version 标签里靠 properties 覆盖内置属性这种黑魔法省下的是三行代码埋下的是每次接手的人都要重新困惑一遍。自查打卡在你的项目里跑一下这段命令grep -rn --includepom.xml -A2 artifactId . | grep -B1 version看看子模块里藏着多少个硬编码的version打在评论区我看看谁的“版本地雷”最多。写在后面我又回去看了一眼根 POM 那行easyexcel.version3.1.1/easyexcel.version。它写在那格式规范命名清晰引用方式挑不出一点毛病。它是这个项目里最无辜的一行配置——每一版依赖升级评估都会有人读到它也每一次都有人被它骗过去。POM 文件里写错的版本号反而好办——编译会报运行会炸总有人来修。真正危险的是这种写了没人看的它安静地维持着一切尽在管理的假象直到某天安全通报下来你对着它改了半天线上版本却纹丝不动。最后问一句你们团队的 Maven 多模块工程是严格坚守“所有版本必须在根 POM 管理”的教条还是像这个项目一样子模块随便硬编码只要能跑就行 有没有被这种“版本不一致”坑过评论区聊聊。下期预告《新增操作员默认密码 123456MD5 存储直接插库》刚才还在聊 Maven 配置的失控下一篇我们去金融项目里看看更让人后背发凉的底线问题。一个给银行做对账的金融级系统创建账号时代码是怎么写的答案极其简单粗暴默认密码全是 123456用不安全的 MD5 加密甚至连加盐Salt都没有直接拼成 SQL 插进数据库。如果你以为这只是个例那明天咱们去看看老系统里还藏着多少这种“掩耳盗铃”的安全漏洞。关联阅读《一个 PDF 功能仓库里躺着四套生成技术栈》——同一个项目里的另一笔依赖烂账《商业授权文件和 Nacos 密码都进了 Git 仓库》——这个项目的仓库里还藏着什么