适用读者:项目能编译但跑起来报
NoSuchMethodError/NoClassDefFoundError;或者依赖树里同名库出现三个版本的人。
前置:S01-B03(Maven 基础,含插件锁定)。
实测环境:Maven 3.8.7 / JDK 17 / Ubuntu 24.04.1;依赖树、exclusion、analyze 输出均为 2026-11-05 实测。
覆盖旧文:无直接旧卡(A-1398 的依赖段在 B03 已讲基础,A-1399 的私服段在 J08 讲发布);本篇为 E 驱动(E-S01-023 依赖调解、E-S01-024 BOM 治理、E-S01-025 私服策略)。
上一篇:S01-J06 · 编码与字符集排障:一次讲清乱码的四种成因
下一篇:S01-J08 · 制品发布:GPG 签名与中央仓库发布。
说明:本篇属"补缺口"文章。旧文没有讲依赖冲突,但这是 Java 后端最常见的运行时事故来源之一。
1. 一句话结论
Maven 只允许同一个 groupId:artifactId 在最终 classpath 上出现一个版本,选谁由规则决定;规则你不懂,就等于把版本交给运气。
三条规则(记住顺序):
- 路径最短者胜(nearest-wins):直接依赖永远赢过传递依赖。
- 同深度时,先声明者胜(first declaration wins)。
- 显式
<exclusions>或<dependencyManagement>可以推翻前两条。
2. 冲突从哪来
| 来源 | 例子 | 后果 |
|---|---|---|
| 直接 vs 传递 | 你声明 slf4j-api:2.0.13,logback-classic 传递依赖 2.0.7 |
传递版本被丢弃(通常无害) |
| 两条传递路径版本不同 | A→C:1.0,B→C:2.0 | 一个被静默丢弃 |
| 同库不同 groupId 的"李鬼" | javax.annotation vs jakarta.annotation |
同时存在于 classpath,NoClassDefFoundError |
| BOM 与直接声明冲突 | 父 POM 的 dependencyManagement 覆盖子模块版本 |
你以为用的是自己写的版本 |
最危险的一类:编译期无错、启动/运行期才炸。因为编译只用"你声明的那份 API",运行时 classpath 上可能是另一份。
3. 实测:看不见的冲突长什么样
3.1 常规 dependency:tree:只显示"胜者"
<dependency>
<groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.13</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.14</version>
</dependency>
$ mvn -B dependency:tree
[INFO] demo:dep-demo:jar:1.0
[INFO] +- org.slf4j:slf4j-api:jar:2.0.13:compile
[INFO] \- ch.qos.logback:logback-classic:jar:1.4.14:compile
[INFO] +- ch.qos.logback:logback-core:jar:1.4.14:compile
看着很干净——但 logback 本来要的 slf4j 版本被藏起来了。
3.2 -Dverbose:把"被丢弃的"也列出来(实测)
$ mvn -B dependency:tree -Dverbose
[INFO] +- org.slf4j:slf4j-api:jar:2.0.13:compile
[INFO] \- ch.qos.logback:logback-classic:jar:1.4.14:compile
[INFO] +- ch.qos.logback:logback-core:jar:1.4.14:compile
[INFO] \- (org.slf4j:slf4j-api:jar:2.0.7:compile - omitted for conflict with 2.0.13)
omitted for conflict with 2.0.13 这行是排障的关键:它明确告诉你
- 被丢弃的版本:
2.0.7 - 胜出者:
2.0.13 - 原因:路径更短(直接依赖)
排障第一步永远是 -Dverbose,不是 dependency:tree。
3.3 排除:让"我不需要的传递依赖"彻底消失(实测)
<dependency>
<groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.14</version>
<exclusions>
<exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></exclusion>
</exclusions>
</dependency>
$ mvn -B dependency:tree -Dverbose
[INFO] +- org.slf4j:slf4j-api:jar:2.0.13:compile
[INFO] \- ch.qos.logback:logback-classic:jar:1.4.14:compile
[INFO] \- ch.qos.logback:logback-core:jar:1.4.14:compile ← slf4j-api 传递项已消失
适用场景:传递依赖带进来一个你不想要的"旧版本/另一个实现"(如同时存在 log4j-slf4j-impl 与 logback-classic → 启动报 Class path contains multiple SLF4J bindings)。
3.4 dependency:analyze:找出"声明了没用"和"用了没声明"(实测)
$ mvn -B dependency:analyze
[WARNING] Unused declared dependencies found:
[INFO] BUILD SUCCESS
| 报告项 | 含义 | 处置 |
|---|---|---|
| Used undeclared | 代码里用了,但没在 pom 声明(靠传递依赖"蹭"来的) |
必须显式声明(否则上游一升级就断) |
| Unused declared | 声明了但代码没直接用 | 确认是否运行期需要(如 JDBC 驱动),否则移除 |
前提:
dependency:analyze需要先能编译。实测中它先失败了——因为项目没锁maven-compiler-plugin版本,报Source option 5 is no longer supported(与 S01-B03 §4.2 是同一个坑)。锁上3.13.0后即通过。这就是 B03 那条"锁插件版本"纪律的第二个证据。
4. 治理三件套
4.1 BOM:把版本收敛到一处(E-S01-024)
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.6</version>
<type>pom</type><scope>import</scope>
</dependency>
<!-- 公司内部 BOM -->
<dependency>
<groupId>com.example</groupId><artifactId>platform-bom</artifactId>
<version>1.0.0</version><type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
| 要点 | 说明 |
|---|---|
dependencyManagement 只约束版本,不引入依赖 |
子模块仍需声明 groupId/artifactId(不写 version) |
| 导入顺序 | 先声明的赢;内部 BOM 放前面可覆盖 Spring 的默认版本 |
| 子模块显式写 version | 会覆盖 BOM —— 这正是"版本不受控"的常见入口 |
| 单模块项目 | 直接 spring-boot-starter-parent 更省事 |
4.2 enforcer:把"禁止的依赖"变成构建失败
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions><execution><id>enforce</id><goals><goal>enforce</goal></goals>
<configuration><rules>
<dependencyConvergence/> <!-- 同库多版本 → 失败 -->
<banDuplicatePomDependencyVersions/>
<requireMavenVersion><version>[3.9,)</version></requireMavenVersion>
<requireJavaVersion><version>[17,)</version></requireJavaVersion>
</rules></configuration>
</execution></executions>
</plugin>
dependencyConvergence 的价值:把"静默丢版本"变成"构建失败并指出冲突双方"。代价是引入了更严格的规则,存量项目接入时要先清一批冲突。
4.3 私服(Nexus / Artifactory,E-S01-025)
| 能力 | 为什么需要 |
|---|---|
| 代理公网仓库 | 加速 + 断网可用 + 统一出口 |
| 托管内部构件 | 内部库不走公网 |
| 快照/发布仓库分离 | 避免快照污染生产 |
| 权限与审计 | 谁知道拉了哪个版本 |
与 B03 §7.4 的联动坑:mirrorOf=* 会把私服也劫持到公网镜像,导致内部构件永远拉不到。正确写法:
<mirror>
<id>corp-nexus</id>
<url>https://nexus.example.com/repository/maven-public/</url>
<mirrorOf>*,!internal-snapshots</mirrorOf>
</mirror>
5. 排障流程:运行时报 NoSuchMethodError
NoSuchMethodError / NoClassDefFoundError / AbstractMethodError
│
1. mvn dependency:tree -Dverbose | grep <包名>
│ → 看到多个版本?还是只有一个但版本不对?
↓
2. mvn dependency:tree -Dincludes=<groupId>:<artifactId>
│ → 精确定位"谁把它带进来的"
↓
3. 决定处置:
│ · 直接声明正确版本(nearest-wins 让它赢)
│ · 对不需要的路径加 <exclusions>
│ · 用 BOM 统一版本
↓
4. 验证:mvn -q dependency:tree -Dverbose 无 omitted 冲突;
│ 打包后 jar 里该库只有一个版本(unzip -l target/*.jar | grep)
↓
5. 防复发:加 enforcer dependencyConvergence;CI 上跑 analyze
第 4 步的 jar 检查最容易被跳过:依赖树干净不代表打包干净(shade/assembly 插件可能把多个版本都打进去)。
unzip -l target/app.jar | grep -E "slf4j-api-[0-9]" # 期望只有一个版本
6. 五项实践纪律
| # | 纪律 | 收益 |
|---|---|---|
| 1 | 锁 maven-compiler-plugin 等核心插件版本 |
构建可复现(B03 §4) |
| 2 | 所有版本集中在 BOM / dependencyManagement |
升级只需改一处 |
| 3 | CI 跑 dependency:analyze + enforcer:dependencyConvergence |
冲突在合并前暴露 |
| 4 | 禁止 SNAPSHOT 进生产制品 |
制品不可变(J08、A02) |
| 5 | 私服 mirrorOf 显式排除内部仓库 |
避免 B03 §7.4 的坑 |
7. 自测
dependency:tree与-Dverbose的差别是什么?排障先跑哪个?- 两条传递路径版本不同且深度相同时,谁赢?怎么改?
Used undeclared为什么危险?dependencyConvergence让构建失败,这算"变严格"还是"变可靠"?为什么?- 依赖树干净但运行时仍报
NoSuchMethodError,下一步查什么?
8. 下一篇
S01-J08 · 制品发布:GPG 签名与中央仓库发布
依赖治理到位后,最后一个进阶层问题是"把自己的库发出去"。下一篇基于旧文 A-1399(849 个点位,本系列第二大篇)完整走一遍发布链路:GPG 签名、pom 元数据要求、staging 校验、以及"发布失败到底该看哪一行日志"。
本篇属于系列《S01 · 开发环境与工具链》。扩展点 E-S01-023/024/025。依赖调解、-Dverbose 冲突行、exclusion、analyze 输出均为 2026-11-05 实测。