☰
Maven 多模块实战:根 POM 版本号不生效?揭秘依赖版本管理失控的乱象
2026/10/21 20:51:25 网站建设 项目流程

栏目:老炮踩坑录 · 翻车现场系列
项目:基础档案 (企业真实项目)
编号:ARC-F03
素材:根pom.xml、子模块pom.xml

👋 欢迎阅读

🏠个人主页:知守观
📘我的专栏:老炮踩坑录
💻当前内容:Maven 版本管理的“潜规则”

文章目录

    • 现场
    • 机制:子模块一旦硬编码,根 POM 就管不着
    • 怎么演变成这样的
    • 付出的代价
    • 怎么改
    • 写在后面

我一直以为,多模块工程的根 POM 是版本号的真正源头。properties 元素里定义,dependencyManagement 元素里引用,子模块声明依赖时不带版本号,一切都是由根 POM 说了算。

教科书上是这么写的。

直到我打开基础档案这个项目,把三个 pom.xml 文件摆在一起对了一遍,发现根 POM 里那些版本号,一部分根本没生效,一部分跟子模块实际用的对不上。最过分的一个,连 Maven 内置属性都被改了名。

现场

先看根 POM 的 properties:

<properties> <java.version>1.8</java.version> <nacos.version>0.2.12</nacos.version> <easyexcel.version>3.1.1</easyexcel.version> <hutool.version>5.7.7</hutool.version> <org.mapstruct.version>1.4.1.Final</org.mapstruct.version> <project.version>0.0.4-SNAPSHOT</project.version> <api.version>0.0.2-SNAPSHOT</api.version> </properties>

easyexcel 3.1.1,mapstruct 1.4.1.Final。dependencyManagement 里规规矩矩地用${easyexcel.version}和${org.mapstruct.version}引用,写法挑不出毛病。

再看子模块。

公共模块的依赖声明:

<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.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> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.3.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.3.Final</version> </dependency>

maven-compiler-plugin 的 annotationProcessorPaths 里还有一处 1.5.3.Final:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.3.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin>

要升 mapstruct,要改三个地方,漏一处就会出现编译期和运行期版本不一致的问题。

三组对不上的数字,列成表:

第三组值得单独说。

project.version是 Maven 内置属性,正常情况下等于 POM 自己的 version 标签。这个项目根 POM 的 version 是 0.0.1-SNAPSHOT,properties 里却又定义了一遍,写成 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.Final;web 硬编码,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 --include="pom.xml" -A2 "<artifactId>" . | grep -B1 "<version>"

第二步,用 enforcer 插件把规矩立起来。requireUpperBoundDeps和banDuplicatePomDependencyVersions这两条规则能挡住大部分版本混乱:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>enforce-version-rules</id> <goals><goal>enforce</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 --include="pom.xml" -A2 "<artifactId>" . | grep -B1 "<version>"看看子模块里藏着多少个硬编码的<version>?打在评论区,我看看谁的“版本地雷”最多。

写在后面

我又回去看了一眼根 POM 那行<easyexcel.version>3.1.1</easyexcel.version>。

它写在那,格式规范,命名清晰,引用方式挑不出一点毛病。它是这个项目里最无辜的一行配置——每一版依赖升级评估都会有人读到它,也每一次都有人被它骗过去。

POM 文件里,写错的版本号反而好办——编译会报,运行会炸,总有人来修。真正危险的是这种写了没人看的:它安静地维持着"一切尽在管理"的假象,直到某天安全通报下来,你对着它改了半天,线上版本却纹丝不动。

最后问一句:你们团队的 Maven 多模块工程,是严格坚守“所有版本必须在根 POM 管理”的教条,还是像这个项目一样,子模块随便硬编码,只要能跑就行? 有没有被这种“版本不一致”坑过?评论区聊聊。


下期预告:《新增操作员默认密码 123456,MD5 存储,直接插库》

刚才还在聊 Maven 配置的失控,下一篇我们去金融项目里看看更让人后背发凉的底线问题。

一个给银行做对账的金融级系统,创建账号时代码是怎么写的?答案极其简单粗暴:默认密码全是 123456,用不安全的 MD5 加密,甚至连加盐(Salt)都没有,直接拼成 SQL 插进数据库。

如果你以为这只是个例,那明天咱们去看看,老系统里还藏着多少这种“掩耳盗铃”的安全漏洞。


关联阅读:

  • 《一个 PDF 功能,仓库里躺着四套生成技术栈》——同一个项目里的另一笔依赖烂账
  • 《商业授权文件和 Nacos 密码,都进了 Git 仓库》——这个项目的仓库里还藏着什么

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询