【Maven】依赖冲突如何解决?

准备工作

1、创建一个空工程 maven_dependency_conflict_demo,在 maven_dependency_conflict_demo 创建不同的 Maven 工程模块,用于演示本文的一些点。

什么是依赖冲突?

  • 引入同一个依赖的多个不同版本时,就会发生依赖冲突。

演示 1

步骤:

1、在 maven_dependency_conflict_demo 中创建 2 个基于 Maven 的 JavaSE 项目 maven_00 和 maven_01。

2、在 maven_01 的 pom.xml 文件中引入 junit 4.12 版本。

3、将 maven_01 打包安装到私服。

4、在 maven_00 的 pom.xml 文件中引入 junit 4.13 版本和 maven_01。

结果如下图所示,可以看到,的的确确发生了依赖冲突,而且 Maven 自己处理了依赖冲突。Maven 处理冲突依据的是短路径优先原则,后面会提到。

Maven的依赖调解原则

Maven 在遇到依赖冲突时,会先根据路径最短优先声明顺序优先两大原则处理冲突。

1、短路径优先

当一个间接依赖存在多条引入路径时,引入路径短的会被解析使用。

见演示 1,依赖关系图如下图所示:

附:代码分支 2411291-larryla-maven-name-first。

2、声明顺序优先

如果存在短路径,则优先选择短路径。如果路径相同,则先声明者优先。

演示 2

步骤:

1、在 maven_dependency_conflict_demo 中创建 3 个基于 Maven 的 JavaSE 项目 maven_02、maven_03 和 maven_04。

2、在 maven_03 中引入 junit 4.12 版本。

3、将 maven_03 打包安装到私服。

4、在 maven_04 中引入 junit 4.13 版本。

5、将 maven_04 打包安装到私服。

6、在 maven_02 中引入 maven_03 和 maven_04,查看 junit 版本引入情况。

上面步骤对应的依赖关系图如下:

结果如下:

(1) 若 maven_03 在 maven_04 前面,则 maven_02 中引入的是 junit 4.12

(2) 若 maven_04 在 maven_03 前面,则 maven_02 中引入的是 junit 4.13

附:代码分支 2411291-larryla-maven-name-first。

3、特殊情况

(1) 子 pom 覆盖父 pom。

如果在父工程和子工程引入了相同依赖的不同版本,那么子工程中的版本会覆盖覆盖父工程中的版本。

步骤:

1、在 maven_dependency_conflict_demo 中创建 2 个基于 Maven 的 JavaSE 项目 maven_07 和 maven_08。

2、在 maven_07 中引入 junit 4.13 版本。

3、在 maven_08 重新指定的 junit 为 4.12 版本。

结果如下图所示,可以看到,子工程引入的是 junit 4.12 版本,而不是继承自父工程的 4.13 版本,这说明子工程覆盖了父工程的版本。

(2) 同一个 pom.xml 文件里配置相同资源的不同版本,后配置的覆盖先配置的。

在 maven_07 的 pom.xml 文件中先后引入 MyBatis 3.5.11 和 3.5.7 版本,结果如下图。可以看到,后声明的 3.5.7 版本覆盖了先声明的 3.5.11 版本。

排除依赖

情景

如果仅仅依靠 Maven 来进行依赖调解,在很多情况下是不适用的,这时需要我们手动排除依赖。

举个例子,当前项目存在下面的依赖关系:

Java 复制代码
依赖链路一:A -> B -> C -> X(2.0.0) // dist = 3
依赖链路二:A -> D -> X(1.0.0) // dist = 2

根据路径最短优先原则,X(1.0) 会被解析使用,也就是说 A 中实际用的是 1.0 版本的 X。但是,这样可能会导致错误。

假如 X(2.0) 相较于 X(1.0) 新增了 1 个类和 1 个方法,并且 A 中使用了新增的类和方法,编译不通过。

问题演示

步骤:

1、在 maven_dependency_conflict_demo 中创建 5 个基于 Maven 的 JavaSE 项目 maven_a、maven_b、maven_c 、maven_d 和 maven_x。

2、在 maven_x 的 1.0 版本打包安装到私服的 Release 仓库。

3、将 maven_x 的 2.0 版本打包安装到私服的 Release 仓库。

4、在 maven_c 中引入 maven_x 的 2.0 版本;将 maven_c 打包安装到私服的 Release 仓库。在 maven_b 中引入 maven_c;将 maven_b 打包安装到私服的 Release 仓库。在 maven_a 中引入 maven_b。

5、在 maven_d 中引入 maven_x 的 1.0 版本,将 maven_d 打包安装到私服的 Release 仓库。在 maven_a 中引入 maven_d。

上面步骤对应的依赖关系图如下:

maven_x 1.0.0 版本:

在 2.0.0 版本中,变化如下:

  • 在 BallSports 类中,新增方法 playFootBall。
  • 新增类 SwimmingSports,类中包含方法 FreeStyle。

结果如下图,依据短路径优先原则,在 maven_a 模块中引入了 maven_x 1.0.0 版本,舍弃了 maven_x 2.0.0 版本,所以无法使用 2.0.0 版本中新增的类和方法

解决方案

一般在解决依赖冲突的时候,我们都会优先保留版本较高的,这是因为大部分 jar 在升级的时候都会做到向下兼容。

1、exclusions标签

具体做法是,在 maven_a 的 pom.xml 文件中使用 exclusion 标签显式排除掉 maven_x 1.0.0 版本的依赖。结果如下图所示,成功引入 maven_x 2.0.0 版本,舍弃 1.0.0 版本。

2、optional标签

我们还可以使用 optional 标签,对外隐藏当前所依赖的资源。

具体做法是,在 maven_d 中引入 maven_x 1.0.0 版本时指定 optional 标签值为 true。

结果如下图所示,可以看到,使用 optional 标签可以使 maven_x 1.0.0 版本的 jar 包不再通过 maven_d 传递,也就是 maven_a 在引入 maven_b 时,得不到 maven_x 1.0.0 的 jar 包。

附:代码分支 2411292-larryla-maven-dependency-exclusion。

对比:标签和标签的区别

由上可知,在 maven_a → maven_d → maven_x 这条分支上,我们有两种解决依赖冲突的方法,即 exclusions 标签和 optional 标签。两种方法的本质都是不将 maven_x 1.0.0 jar 包传递到 maven_a 中。但区别在于:

  • 排除依赖是在 maven_a 引入 maven_d 依赖时,同时设置<exclusions>,maven_a 知道 maven_x 1.0.0 的存在,主动将其排除掉。这种适用于不能修改 maven_d 的配置文件的情况
  • 可选依赖是在 maven_d 上为其依赖项 maven_x 设置<optional>,是 maven_d 不让"外界"知道依赖 maven_x 的存在,不再传递依赖 maven_x,此时,maven_a 不知道 maven_x 1.0.0 的存在。这种适用于可以修改 maven_d 的配置文件的情况

思考 1

在"问题演示"中,我们复现了短路径优先可能导致的问题,即 maven_a 会引入 maven_x 1.0.0 版本,舍弃 maven_x 2.0.0 版本。那么问题来了,既然引入了 maven_x 1.0.0 版本,那么 maven_b 和 maven_c 会用哪个版本的 maven_x 呢?

结果如下,由编辑区的方法调用和右侧的依赖管理可以得知,maven_a 为了解决依赖冲突选择引入 maven_x 1.0.0 版本,而 maven_b 和 maven_c 不受 maven_a 的影响,引入的依旧是 maven_x 2.0.0 版本:

因此,上面给出的依赖传递图不是很精确的,应该如下:

  • maven_a 引入 maven_x 1.0.0
  • maven_b、maven_c 引入 maven_x 2.0.0

附:代码分支 2411292-larryla-maven-dependency-exclusion。

思考 2

对于"情景"一节,Maven 在依据短路径优先原则调解冲突时可能会带来一些问题。假如 X(2.0) 相较于 X(1.0) 新增了 1 个类和 1 个方法:

  • A 中使用了新增的类,编译不通过。
  • A 中使用了新增的方法,编译不通过。

我们在"问题演示"一节复现了上面两个问题,并且在"解决方案"一节中使用两种方案 exclusions 和 optional,引入了高版本 maven_x 2.0.0 解决了这个问题。

但是,引入高版本就没有问题了吗?

还是下图的依赖关系,假如 maven_a 引入了 maven_x 2.0.0 版本。

假如 maven_x 2.0.0 相比于 maven_x 1.0.0 版本删掉了一个类,但 maven_a 已经引用了,这时会报错;maven_x 2.0.0 相比于 maven_x 1.0.0 版本删掉了一个方法,但 maven_a 已经引用了,同样也会报错。

演示

我们重新组织 maven_x 1.0.0 和 maven_x 2.0.0 版本包括的内容。

maven_x 1.0.0 版本内容如下:

  • BallSports:playBasketBall 方法、playFootBall 方法

  • SwimmingSports:FreeStyle 方法

    maven_x 1.0.0 版本内容变化如下:

  • 删掉 BallSports 类中的 playBasketBall 方法

  • 删掉 SwimmingSports 类

步骤:

1、在 maven_x 的 1.0 版本打包安装到私服的 Release 仓库。

2、将 maven_x 的 2.0 版本打包安装到私服的 Release 仓库。

3、在 maven_c 中引入 maven_x 的 2.0 版本;将 maven_c 打包安装到私服的 Release 仓库。在 maven_b 中引入 maven_c;将 maven_b 打包安装到私服的 Release 仓库。在 maven_a 中引入 maven_b。

4、在 maven_d 中引入 maven_x 的 1.0 版本,将 maven_d 打包安装到私服的 Release 仓库。在 maven_a 中引入 maven_d。

结果如下,可以看到 maven_a 中引入的是 maven_x 2.0.0 版本,无法使用 2.0.0 版本相比 1.0.0 版本删掉的方法和类,直接编译不通过。

附:代码分支 2411301-larryla-maven-dependency-exclusion-high-ver-problem。

聚合工程,统一管理版本

聚合工程:一个项目允许创建多个子模块,多个子模块组成一个整体,可以统一进行项目的构建。要弄明白聚合工程,得先清楚"父子工程"的概念:

  • 父工程:没有任何代码、仅有pom.xml的空项目,用来定义公共依赖、插件和配置;
  • 子工程:编写具体代码的子项目,可以继承父工程的依赖项、配置,还可以独立拓展。

而 Maven 聚合工程,就是基于父子工程结构,将一个完整项目,划分出不同的层次。这种方式可以很好的管理多模块之间的依赖关系,以及构建顺序,大大提高了开发效率、维护性。

为了防止不同子工程引入不同版本的依赖,在父工程中,统一对依赖的版本进行控制,规定所有子工程都使用同一版本的依赖 ,可以使用<dependencyManagement>标签来管理。

子工程在使用<dependencyManagement>中已有的依赖项时,不需要写<version>版本号,版本号在父工程中统一管理,这样做的好处在于:以后为项目的技术栈升级版本时,不需要单独修改每个子工程的POM,只需要修改父POM文件即可,减少了版本冲突的可能性。

演示

步骤:

1、在 maven_dependency_conflict_demo 中创建 2 个基于 Maven 的 JavaSE 项目 maven_05 和 maven_06。

2、在 maven_05 中引入 junit 4.13 版本,使用 dependencyManagement 标签统一管理。

3、在 maven_06 继承 maven_05 的 junit 4.13 版本。

上面步骤对应的依赖关系图如下:

结果如下图所示,可以看到,在 maven_06 工程中,只需要指定 groupId 和 artifactId,不需要指定 version,就可以从父工程 maven_05 中继承 junit 4.13 依赖。

其他

1、子工程继承父工程的依赖时,可以指定版本,这时子工程的版本会覆盖父工程的版本。

演示:

还是使用"演示"一节的模块。在 maven_06 模块中指定 junit 的版本为 4.12,那么 maven_06 引入的 junit 将是 4.12 版本,而不是父工程的 4.13 版本:

虽然可以在子工程中指定其他版本,但不建议这么做。如果这样做的话,使用 dependencyManagement 的意义就不大了。

2、父工程可以使用 properties 标签指定依赖的版本,然后在 GAV 坐标中引入该版本。这样在升级依赖版本时,只需要修改 properties 标签中变量的值,而无需修改 GAV 中的 version 标签的值。

演示:

还是使用"演示"一节的模块。在 maven_05 的 pom.xml 文件中,在 properties 标签中自定义一个 junit-version 标签,用于指定 junit 依赖的版本号。然后,在 version 标签中使用 ${junit-version} 的形式声明版本号。如下图所示,maven_06 依旧成功引入了 junit 4.13 版本。

3、dependencyManagement 标签只是用于声明依赖,不会在项目中引入依赖。

如下图所示,只是在聚合工程的子工程中引入了 junit 依赖,父工程并未引入依赖。

4、dependencyManagement 和 dependencies 的区别

  • <dependencies>:定义强制性依赖,写在该标签里的依赖项,子工程必须继承。
  • <dependencyManagement>:定义可选性依赖,该标签里的依赖项,子工程可选择性地继承。

附:代码分支 2411302-larryla-maven-dependency-uni-manage

排查依赖冲突

在 MySQL 中,我们会借助一些工具进行慢 SQL 分析。那么,在 Maven 中,我们是否可以借助一些工具进行依赖冲突定位呢?

我们可以在插件市场下载"Maven Helper" 插件辅助排查依赖冲突。

在"排除依赖-情景"一节中,我们分析了如下依赖冲突情况,得到的结论是 Maven 会依据短路径优先原则引入 X(1.0) 版本。

Java 复制代码
依赖链路一:A -> B -> C -> X(2.0.0) // dist = 3
依赖链路二:A -> D -> X(1.0.0) // dist = 2

接下来,我们使用 Maven Helper 插件来验证一下。

打开 maven_a 的 pom.xml 文件,在下方点击 Dependency Analyzer,然后点击 Refresh UI,会弹出如下界面:

提示有一个依赖冲突,即 maven_x: 1.0.0。我们点开看看,发现与之冲突的是 maven_x 2.0.0。这就和上面呼应上了,Maven 依据短路径优先原则,选择了 1.0.0 版本,舍弃了 2.0.0 版本。

在"排除依赖-解决方案"一节中,我们基于排除依赖引入了 2.0.0 版本,这里,我们同样保留 2.0.0 版本。在 1.0.0 上右键,选择 exclude,表示排除掉该版本。然后点击上面的 Reimport,再点击 Refresh UI。

返回到 pom.xml 文件,发现点击上面的 exclude 按钮,就是在 pom.xml 文件中使用了 exclusions 标签:

思维导图

代码仓库:git@github.com:Acura-bit/maven_dependency_conflict_demo.git

相关推荐
float_com3 小时前
【JavaWeb】----- Maven入门与实践
maven·javaweb
阿宁又菜又爱玩4 小时前
Maven基础知识
java·maven
冷雨夜中漫步4 小时前
Maven BOM(Bill of Materials)使用指南与常见错误
java·数据库·maven
BUTCHER54 小时前
maven插件
java·maven
Hui Baby6 小时前
maven自动构建到镜像仓库
java·maven
永不停歇的蜗牛21 小时前
Maven的POM文件相关标签作用
服务器·前端·maven
洛克大航海1 天前
Maven 的下载安装配置教程
java·maven
Mr.朱鹏1 天前
RocketMQ可视化监控与管理
java·spring boot·spring·spring cloud·maven·intellij-idea·rocketmq
咖啡不甜不好喝1 天前
IDEA Maven设置所有项目生效
maven·idea
0和1的舞者1 天前
《Maven 核心功能与仓库体系详解》
ide·maven·项目管理·仓库·依赖