乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 介绍与核心概念

    • Maven是什么
    • 约定优于配置
  • 第2章 安装与配置

    • 安装与验证
    • settings.xml
    • 本地仓库与镜像
  • 第3章 POM与项目坐标

    • POM
    • GAV坐标
    • packaging
  • 第4章 标准目录布局

    • 标准目录布局
  • 第5章 依赖机制

    • dependencies
    • scope
    • 依赖传递
    • 依赖冲突与调解
    • exclusions
    • optional
    • dependencyManagement
  • 第6章 仓库

    • 仓库体系
    • 本地仓库
    • 远程仓库与镜像
    • 私服
  • 第7章 构建生命周期

    • 生命周期概述
    • clean 生命周期
    • default 生命周期
    • site 生命周期
    • 生命周期与插件绑定
  • 第8章 插件

    • 插件概述
    • maven-compiler-plugin
    • maven-surefire-plugin
    • maven-war-plugin
  • 第9章 继承与聚合

    • parent继承
    • 聚合
    • BOM
    • properties
  • 第10章 属性与资源过滤

    • 资源过滤
    • Profile
  • 第11章 常用命令

    • mvn compile
    • mvn test
    • mvn package
    • mvn clean
    • mvn install
    • mvn dependency:tree
  • 第12章 常见问题与最佳实践

    • 依赖冲突排查
    • 最佳实践

依赖冲突与调解

本章承接"scope"和"依赖传递",深入讲解 Maven 的依赖冲突调解机制。理解"最短路径优先"和"先声明优先",是排查"NoSuchMethodError""ClassNotFoundException"等运行时诡异错误的必备技能。


核心机制

当项目的依赖树中存在同一个构件的多个版本时,Maven 使用一套确定性算法来决定哪个版本进入最终 classpath,以确保构建的可复现性。

这句话的关键词是确定性算法。Maven 不是随机选一个版本,而是按照固定规则调解,让你可以通过调整 POM 来预测和控制结果。

冲突是如何产生的?

依赖冲突是传递依赖的副作用。当你的项目依赖了 A 和 B,而 A 传递了 C-1.0,B 传递了 C-2.0,classpath 里就出现了 C 的两个版本——这就是冲突。

你的项目
├── A → C:1.0
└── B → C:2.0

Java 的 classpath 不允许同一个类的两个版本共存(包名和类名相同)。Maven 必须做出选择:保留 1.0 还是 2.0?

调解规则一:最短路径优先(Nearest Definition)

Maven 首先比较两个版本在依赖树中的深度(距离你的项目的边数):

  • 深度小的优先(路径短的优先)
  • 如果深度相同,进入规则二

示例:

你的项目
├── A → C:1.0          # 路径长度 2
└── B → D → C:2.0      # 路径长度 3

C:1.0 的路径长度是 2(你的项目 → A → C),C:2.0 的路径长度是 3(你的项目 → B → D → C)。根据最短路径优先,C:1.0 被选中。

调解规则二:先声明优先(First Declaration)

如果两个版本的路径长度相同,Maven 比较它们在 pom.xml 中直接依赖的声明顺序:

  • 先声明的直接依赖,其传递版本优先

示例:

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>A</artifactId>
        <version>1.0</version>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>B</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>
你的项目
├── A → C:1.0          # 路径长度 2,A 先声明
└── B → C:2.0          # 路径长度 2,B 后声明

C:1.0 被选中,因为 A 在 pom.xml 中先于 B 声明。

被排除的版本去哪了?

被 Maven 排除的版本不会进入 classpath,但它仍然会被下载到本地仓库(因为解析依赖树时需要)。只是最终编译、测试、打包时,使用的是被调解后的版本。

生活类比:家族继承中的"最近血缘优先"

想象你在处理家族遗产:

  • 最短路径优先:你爷爷有两个儿子(A 和 B),A 的儿子(C-1.0)和 B 的孙子(D 的儿子 C-2.0)都来继承。法律规定"直系亲属优先"——A 的儿子(路径 2:你 → A → C)比 B 的孙子(路径 3:你 → B → D → C)更近,所以 A 的儿子优先继承。
  • 先声明优先:如果 A 和 B 都有儿子叫 C(路径都是 2),但 A 在户口本上排在 B 前面(先声明),A 的儿子优先。

Maven 的调解算法就像这套"血缘+户口本顺序"的继承法——规则明确、可预测、无歧义。


图示

冲突场景与调解过程

上图展示了一个典型冲突场景:你的项目同时依赖 A 和 B,A 传递了 C:1.0(路径长度 2),B 通过 D 传递了 C:2.0(路径长度 3)。Maven 根据最短路径优先,选择 C:1.0,C:2.0 被排除在最终 classpath 之外。

先声明优先场景

当路径长度相同时,Maven 退回到 pom.xml 中的声明顺序。这个设计意味着:你可以通过调整 <dependency> 的顺序来影响调解结果(虽然更推荐显式声明版本)。


完整示例

场景

飞翔科技的 employee-system 同时依赖 spring-context 5.3.21 和 mybatis 3.5.9。后端小崔发现 mybatis 传递了 slf4j-api 1.7.32,而 spring-context 的某个间接依赖传递了 slf4j-api 2.0.0。两个版本的 slf4j-api 同时出现在依赖树中,小崔需要理解 Maven 如何调解,并验证最终 classpath。

操作前:冲突爆发

小崔的 pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.21</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
</dependencies>

执行依赖树分析:

mvn dependency:tree -Dverbose

输出(节选):

com.feixiang:employee-system:jar:1.0.0
+- org.springframework:spring-context:jar:5.3.21:compile
|  +- org.springframework:spring-core:jar:5.3.21:compile
|  |  \- (org.slf4j:slf4j-api:jar:1.7.32:compile - omitted for conflict with 2.0.0)
|  \- ...
\- org.mybatis:mybatis:jar:3.5.9:compile
   \- org.slf4j:slf4j-api:jar:2.0.0:compile

注意括号里的 omitted for conflict with 2.0.0——spring-core 传递的 slf4j-api 1.7.32 被省略了,因为 mybatis 直接传递的 slf4j-api 2.0.0 路径更短(长度 2 vs 长度 3)。

潜在风险:slf4j-api 2.0.0 和 1.7.32 存在 API 不兼容。如果 spring-core 的代码调用了 1.7.32 中存在但 2.0.0 中已删除的方法,运行时会抛出 NoSuchMethodError。

操作后:显式锁定版本

白歌指导小崔在 pom.xml 中直接声明 slf4j-api 的版本,覆盖传递版本:

<dependencies>
    <!-- 显式声明 slf4j-api,路径长度=1,必然优先 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.32</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.21</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
</dependencies>

再次执行:

mvn dependency:tree -Dverbose

输出:

com.feixiang:employee-system:jar:1.0.0
+- org.slf4j:slf4j-api:jar:1.7.32:compile
+- org.springframework:spring-context:jar:5.3.21:compile
|  +- org.springframework:spring-core:jar:5.3.21:compile
|  |  \- (org.slf4j:slf4j-api:jar:1.7.32:compile - omitted for duplicate)
|  \- ...
\- org.mybatis:mybatis:jar:3.5.9:compile
   \- (org.slf4j:slf4j-api:jar:2.0.0:compile - omitted for conflict with 1.7.32)

变化分析:

  • 小崔直接声明的 slf4j-api 1.7.32 路径长度为 1,比任何传递版本都短,必然胜出
  • spring-core 传递的 1.7.32 被标记为 omitted for duplicate(重复,已存在)
  • mybatis 传递的 2.0.0 被标记为 omitted for conflict with 1.7.32(冲突,被覆盖)
  • 最终 classpath 中只有 slf4j-api 1.7.32,运行时兼容性得到保障

李眉部署后,系统运行稳定,没有再出现 NoSuchMethodError。


易错点与常见问题

误区一:Maven 会自动选择"最新版本"

错误认知:"C:1.0 和 C:2.0 冲突,Maven 肯定选 2.0,因为更新。"

纠正:Maven 不看版本号大小,只看路径长度和声明顺序。2.0 并不比 1.0 更优先。在上面的例子中,如果 mybatis 被声明在 spring-context 前面,且路径长度相同,mybatis 传递的版本就会胜出——无论它是 1.0 还是 2.0。

误区二:被排除的版本完全不存在

错误认知:"mvn dependency:tree 显示 C:2.0 被 omitted,所以它没被下载。"

纠正:被排除的版本仍然会被下载到本地仓库,因为 Maven 解析依赖树时需要读取它的 POM 来确认传递关系。只是它不进入最终 classpath。如果你查看 ~/.m2/repository,会发现 C 的两个版本文件夹都存在。

误区三:调解规则可以覆盖所有冲突

错误认知:"我理解了最短路径和先声明,就能解决所有依赖冲突。"

纠正:调解规则只能决定哪个版本进入 classpath,不能解决API 不兼容。如果 C:1.0 和 C:2.0 的 API 差异很大,无论选哪个,都可能让依赖另一个版本的项目报错。此时需要:

  1. 升级/降级直接依赖,让它们统一传递同一个版本
  2. 使用 exclusions 切断某条传递路径
  3. 使用 dependencyManagement 统一锁定版本

小结

Maven 的依赖冲突调解遵循两条确定性规则:最短路径优先和先声明优先。这套算法确保了构建的可复现性,但也意味着开发者需要主动审查依赖树(mvn dependency:tree),发现冲突后通过显式声明、排除或版本锁定来消除风险。依赖冲突是传递依赖的必然副作用,理解调解机制是保障运行时稳定性的关键。

本章与全局的关系:本章解释了"冲突时如何选择版本"。下一章"exclusions"将讲解"如何主动切断不需要的传递依赖"——这是精确控制依赖树的重要工具。

上一页
依赖传递
下一页
exclusions