news 2026/10/19 4:59:41

记一次诡异的 Maven 依赖冲突:pom 里写着 2.0,打出来的包却是 1.5

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
记一次诡异的 Maven 依赖冲突:pom 里写着 2.0,打出来的包却是 1.5

老炮踩坑录 · 番外篇 · 翻车现场系列
基于「企业融合评估平台」真实 pom 与打包产物,复盘 Maven 冲突时怎么决定用哪个版本
关键词:依赖冲突 · 传递依赖 · nearest-wins · dependencyManagement · NoSuchMethodError

👋 欢迎阅读

🏠个人主页:知守观
📘我的专栏:老炮踩坑录
💻当前内容:Maven 依赖冲突

文章目录

    • 前言
    • 案发现场:pom 里写的和 lib 里跑的不一致
    • 版本是谁定的:Maven 的三条选择规则
      • 规则一:dependencyManagement 说了算
      • 规则二:路径最近者胜(nearest-wins)
      • 规则三:同深度,先声明的胜;重复声明,只打一行警告
    • 为什么没炸,以及为什么难发现
    • 正确写法:四条铁律
      • 铁律一:看依赖树,看打包产物,别只看 pom
      • 铁律二:关键版本,用 dependencyManagement 锁定
      • 铁律三:把检查交给构建工具,别交给人的眼睛
      • 铁律四:同族依赖,版本对齐
    • 自查清单
    • 老炮点评
    • 下期预告

前言

离职后复盘这个项目,有一天我解压了当年打的 war 包,想核对几个 jar 的版本。

翻到WEB-INF/lib目录,一个文件名让我停住了:

jsqlparser-1.0.jar

1.0?项目里的 mybatis-plus 是 3.3.0,它那一版的发布说明里白纸黑字写着 “jsqlparser up 3.1”。也就是说 mybatis-plus 要带进来的是 jsqlparser 3.1。

lib 目录里只有一个 jsqlparser,是 1.0,3.1 不见了。

谁换的?没人换呀。pom.xml 里从头到尾没出现过 jsqlparser 这个词——它是传递依赖,两个框架各自依赖了不同的版本,Maven 按规则保留了其中一个。

选择的结果写在 lib 目录里,只是从来没人去看。

案发现场:pom 里写的和 lib 里跑的不一致

pom.xml 里的两段声明(真实行号):

<!-- pom.xml L94 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.0</version></dependency><!-- pom.xml L102 --><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.2.5</version></dependency>

两个都是分页相关的框架,同时存在于一个项目里。它们各自拖着一条传递依赖链,终点是同一个 artifact:
同一个com.github.jsqlparser:jsqlparser,一个依赖 1.0,一个依赖 3.1。Maven 的处理方式是自己选一个,不报错——选一个保留,另一个不打进 classpath,也不给任何提示。

最终保留下来的,是 pagehelper 那条链上的 1.0。

注意一个细节:pom 里 mybatis-plus 声明在第 94 行,pagehelper 在第 102 行,mybatis-plus 靠前。很多人以为"先声明的赢",mybatis-plus 明明先声明,还是输了。因为 Maven 比的是依赖路径的深度,3 比 4 小,pagehelper 那条链更近,声明顺序在这里不起作用。

版本是谁定的:Maven 的三条选择规则

这次复盘我把 pom.xml 和 lib 目录从头到尾对了一遍,版本不一致的情况一共三类,对应 Maven 的三条规则。

规则一:dependencyManagement 说了算

pom.xml 里这个依赖,一个版本号都没写:

<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency>

lib 里躺的是mysql-connector-java-8.0.13.jar。版本号从哪来的?从 parent 来——spring-boot-starter-parent 2.1.0背后的spring-boot-dependencies这份 BOM,把几百个常用依赖的版本全管了。

再看配置文件里的痕迹:
com.mysql.cj.jdbc.Driver是 8.x 驱动才有的类名,serverTimezone是 8.x 驱动要求的参数。这个项目跑的是 8.0.13 的驱动,而决定这件事的人,是写 spring-boot-dependencies 的人。

不写版本本身没错,BOM 就是干这个的。但要清楚一点:把 parent 从 2.1.0 升到 2.7.18,pom 一个字不用改,驱动、Jackson、Netty、Logback 的版本全跟着变。升级 parent 从来不只是升一个框架,是换掉整张依赖清单。

规则二:路径最近者胜(nearest-wins)

jsqlparser 这一局就是这条规则的实例。两条链,深度 3 的赢,深度 4 的被丢弃。

被丢弃的那个不是消失了,Maven 只是不打进 classpath,在依赖树里会留一行记录。这是排查的关键证据,后面讲怎么查到这条记录。

规则三:同深度,先声明的胜;重复声明,只打一行警告

这份 pom 里,commons-lang:commons-lang:2.6声明了两遍(L243 和 L265),一个字不差的两遍。commons-codec也声明了两遍:第一次不写版本(被 BOM 管成 1.11),第二次显式写 1.11,注释写着"华为云使用"。

Maven 对重复声明的处理是:用其中一个,然后在构建日志里打一行[WARNING] duplicate declaration。

一行 warning。构建日志几千行,没人看。这两处重复声明从进仓库那天起就一直在,到项目停更也没人发现。

为什么没炸,以及为什么难发现

先说没炸的原因,如实讲。

我搜了全项目,分页代码 10 个文件,清一色PageHelper.startPage:

PageHelper.startPage(pageNum,pageSize);

mybatis-plus 在这个项目里只被当 CRUD 框架用——没配PaginationInterceptor,没有任何地方调selectPage,IPage一个引用都没有。jsqlparser 3.1 的 API 主要被 mybatis-plus 的分页 count 优化用到,而那段代码在这个项目里从来没被执行过。

pagehelper 自己做分页用 1.0,正好 classpath 上就是 1.0,严丝合缝。需要 3.1 的代码路径,一步都没踩上去。

网上搜 “The following method did not exist: net.sf.jsqlparser”,能搜到一大把 pagehelper 和 mybatis-plus 共存的事故记录,症状都差不多:日常功能全好,某天一条复杂 SQL 触发分页 count 优化,抛NoSuchMethodError。具体是哪个方法不存在,取决于那条 SQL 解析出来什么结构。

这类问题难发现,原因有四个:

  • 编译期不校验。编译只看你 import 的类在不在,不检查运行时会加载哪个版本。jsqlparser 1.0 和 3.1 包名一样,编译两边都过。
  • 启动期不校验。Spring 容器起来,一切正常。版本不对不会阻止启动,只会在执行到那行代码时炸。
  • 测试走不到。测试环境的 SQL 简单,分页 count 不走优化分支,或者干脆没测那条路径。
  • 没人看产物。所有人核对依赖都看 pom.xml,没人解压 war 包看WEB-INF/lib。而 pom 里写的,本来就不是最终结果。

正确写法:四条铁律

铁律一:看依赖树,看打包产物,别只看 pom

mvn dependency:tree-Dverbose-Dincludes=com.github.jsqlparser

-Dverbose是关键,被丢弃的版本会显示出来:

omitted for conflict with 1.0——谁要它、输给了谁、赢的是哪个版本,一行全交代了。每一行 omitted 背后都是一次版本冲突的选择,挨个搞清楚原因,是接手老项目的第一课。

war 包项目再加一步:解压产物,直接翻WEB-INF/lib。那是上线时类路径上的全部真相。

铁律二:关键版本,用 dependencyManagement 锁定

<dependencyManagement><dependencies><dependency><groupId>com.github.jsqlparser</groupId><artifactId>jsqlparser</artifactId><version>3.1</version></dependency></dependencies></dependencyManagement>

dependencyManagement 优先级高于传递依赖的选择结果,写进去,所有路径上的 jsqlparser 统一成 3.1。

但锁定之前先回答一个问题:两个使用者都兼容你锁的那个版本吗?jsqlparser 1.0 到 3.1 跨了大版本,pagehelper 5.1.4 配 3.1 能不能正常工作,我没有验证过,不敢替它打包票。锁完版本,两个框架的分页功能都要回归一遍。

更根本的问题其实是:一个项目为什么同时需要 mybatis、mybatis-plus、pagehelper 三个持久层相关的 starter?这是选型层面的事,我在 Java 技术选型三条铁律 文章里写过,这里不展开。

有个细节我没完全确认:文章里我提到,把jsqlparser强行锁定到 3.1 后,pagehelper 5.1.4能不能完美兼容,我没有实际测过。有没有兄弟在项目里踩过这俩组合的坑?或者你们干脆把pagehelper换成mybatis-plus自带的分页了?评论区指点一下。

铁律三:把检查交给构建工具,别交给人的眼睛

<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-enforcer-plugin</artifactId><executions><execution><goals><goal>enforce</goal></goals><configuration><rules><dependencyConvergence/><banDuplicatePomDependencyVersions/></rules></configuration></execution></executions></plugin>

dependencyConvergence是最狠的一条:同一个 artifact 从多条链进来,版本必须收敛成一个,否则构建直接失败。banDuplicatePomDependencyVersions治的是 commons-lang 写两遍那种问题,重复声明从 warning 升级成 error。

丑话说在前面:这份 pom 加上dependencyConvergence,第一次构建肯定过不了,jsqlparser、httpclient 那一串 omitted 全会变成报错。这正是目的——把运行时才暴露的版本问题,提前到构建期暴露。修的方式就是铁律二,在dependencyManagement里逐个锁定。

铁律四:同族依赖,版本对齐

pom.xml L131 还有一处:

<dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-guava</artifactId><version>2.9.8</version></dependency>

Jackson 这一族,Boot 2.1.0 的 BOM 统一管在 2.9.7。这里单独给家族里的一个成员指定了 2.9.8。lib 目录里现在的局面是:jackson-databind-2.9.7.jar和jackson-datatype-guava-2.9.8.jar并存,一个家族混着两个补丁版本。

如实说,这次混版本没造成过已知的事故,Jackson 2.9.x 补丁版本之间基本兼容。但写法本身就是隐患——下次有人照这个思路给 databind 也单独指个版本,撞上一组真不兼容的组合,就是另一个 jsqlparser 故事。

同族依赖的处理只有两种:整个家族交给 BOM 管,一个版本都别写;或者整个家族显式指定同一个版本。只给家族里一个成员开小灶,是最差的选择。

自查清单

自查打卡:在你的项目里跑一下这个命令:
mvn dependency:tree -Dverbose | grep "omitted for conflict"
看看有多少个被 Maven 静默丢弃的版本?打在评论区,我看看谁的“地雷”最多。

老炮点评

pom.xml 里写的版本号,只是 Maven 选择版本时的输入之一,不是最终结果。真正决定类路径的是那三条规则:BOM、路径深度、声明顺序。

编译通过、启动成功,什么都证明不了。类路径上躺的是哪个版本,只有打包产物和依赖树说了算。

这个项目的 jsqlparser 没出事,靠的是那段代码从来没被执行。ThreadLocal + @Async 导致用户数据串号 文章里那句话放到这里一样成立:你依赖的是碰巧没触发,而不是代码本身是安全的。

最后问一句:你们平时排查NoSuchMethodError或依赖冲突,是老老实实跑mvn dependency:tree捋,还是等线上真炸了,才被迫去解压 jar 包看版本?
评论区聊聊,我看看有多少项目是“盲盒式”上线。

下期预告

核对版本的过程中我还发现了另一件事:这个 pom 声明了 60 多个依赖,lib 目录里却躺了 150 多个 jar。

多出来的那些是哪来的?翻了一下,一个叫 swagger2markup 的依赖贡献了一大串——rhino、pegdown、json-schema-validator、libphonenumber,全跟着进了WEB-INF/lib。一个只在本地生成离线文档的工具,凭什么上生产?

下期讲:《一个生成文档的依赖,让生产 war 包多了 30 个 jar》。

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注 | 💬 留言

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

我是老炮,18 年 Java 老兵,仍在一线。关注「Java老炮踩坑录」,不错过每一篇真实案例,少踩坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 4:59:27

基于MobileNetV2的轻量级猫狗图像分类端到端实践

几个月前做一个图像预筛模块&#xff0c;甲方提了个很具体的要求&#xff1a;要在普通办公电脑上处理一批动物照片&#xff0c;先把“猫”和“狗”分出来&#xff0c;模型不能太大、推理不能太慢&#xff0c;而且从数据整理到模型导出全流程都要有完整记录。我最后落地的是一个…

作者头像 李华
网站建设 2026/10/19 4:58:52

明文传输漏洞详解:从HTTP到HTTPS的全链路加密实战指南

互联网上每天都在悄悄发生数据被截获的事情&#xff0c;而绝大多数普通人根本察觉不到。你在酒店连的免费Wi-Fi可能正在被监听&#xff0c;打开某个APP看到的图片可能被运营商压缩&#xff0c;登录一个网站时输入的密码可能正以纯文本形式在网络中穿梭。这一切的根源&#xff0…

作者头像 李华
网站建设 2026/10/19 4:58:41

逆向工程入门:通用寄存器与内存读写实战解析

1. 从“寄存器”到“内存读写”的逆向认知路径很多人学逆向&#xff0c;一上来就抱着反汇编引擎啃指令集手册&#xff0c;结果看了三天还在纠结MOV和LEA的区别&#xff0c;最后不了了之。我带过不少刚入门的朋友&#xff0c;发现一个很普遍的误区&#xff1a;把逆向当成“背指令…

作者头像 李华
网站建设 2026/10/19 4:57:51

测试时强化学习:数百美元微调开源模型,推理阶段逆袭闭源大模型

最近圈子里讨论最热的一件事&#xff0c;莫过于某顶尖高校AI实验室与某芯片巨头研究组联合放出的那套“测试时强化学习”方案。核心结果很有冲击力&#xff1a;用几百美元的微调成本&#xff0c;让一个开源底座模型在多项高难度推理任务上反超了顶级闭源商业模型&#xff0c;而…

作者头像 李华
网站建设 2026/10/19 4:57:46

开源代码评审工具open-code-review实战:从上下文聚合到大模型集成

代码评审这件事&#xff0c;做的时间越长越觉得&#xff0c;它不是“看一遍代码”那么简单&#xff0c;而是一套夹在人和人之间的沟通流程。我参与过不少项目的评审&#xff0c;也亲眼见过团队从“贴个diff到群里让大家自取”到“每周固定时间拉会挨个过”&#xff0c;但说实话…

作者头像 李华