1901 字
约 6 分钟
4
S01-J07 · 依赖治理:依赖树、冲突、BOM 与私服

适用读者:项目能编译但跑起来报 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 上出现一个版本,选谁由规则决定;规则你不懂,就等于把版本交给运气。

三条规则(记住顺序):

  1. 路径最短者胜(nearest-wins):直接依赖永远赢过传递依赖。
  2. 同深度时,先声明者胜(first declaration wins)。
  3. 显式 <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. 自测

  1. dependency:tree 与 -Dverbose 的差别是什么?排障先跑哪个?
  2. 两条传递路径版本不同且深度相同时,谁赢?怎么改?
  3. Used undeclared 为什么危险?
  4. dependencyConvergence 让构建失败,这算"变严格"还是"变可靠"?为什么?
  5. 依赖树干净但运行时仍报 NoSuchMethodError,下一步查什么?

8. 下一篇

S01-J08 · 制品发布:GPG 签名与中央仓库发布

依赖治理到位后,最后一个进阶层问题是"把自己的库发出去"。下一篇基于旧文 A-1399(849 个点位,本系列第二大篇)完整走一遍发布链路:GPG 签名、pom 元数据要求、staging 校验、以及"发布失败到底该看哪一行日志"。


本篇属于系列《S01 · 开发环境与工具链》。扩展点 E-S01-023/024/025。依赖调解、-Dverbose 冲突行、exclusion、analyze 输出均为 2026-11-05 实测。

S01-J07 · 依赖治理:依赖树、冲突、BOM 与私服
https://www.clxhxhhr.top/posts/5172/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。