/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
栏目:老炮踩坑录 · 翻车现场系列 项目:基础档案 (企业真实项目) 编号: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 仓库》——这个项目的仓库里还藏着什么




