我们一起聊聊 Maven 依赖冲突问题

开发 前端
一般我们在解决依赖冲突的时候,都会选择保留jar高的版本,因为大部分jar在升级的时候都会做到向下兼容,所以只要保留高的版本就不会有什么问题。

1、简介

1.1、什么是依赖冲突

依赖冲突是指:在 Maven 项目中,当多个依赖包,引入了同一份类库的不同版本时,可能会导致编译错误或运行时异常。

1.2、依赖冲突的原因

我们在 Maven 项目的 Pom 中 一般会引用许许多多的 Dependency。例如,项目A有这样的依赖关系:

A -> C -> X(1.0)
B -> D -> X(2.0)

X是A的 传递性依赖 ,但是两条依赖路径上有两个版本的X,那么哪个X会被 Maven 解析使用呢? 两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。

在绝对大多数情况下,依赖冲突问题并不需要我们考虑,Maven 工具会自动根绝依赖原则选择,这里我们先假设最终引用的 X(1.0) 版本,

1、你想如果B引用 X(2.0) 的新创建的类,但因为最终被解析的是 X(1.0),所以就会出现很典型的 NoClassDefFoundError 或 ClassNotFoundException 依赖冲突报错。

2、如果B引用 X(2.0) 的新创建的方法,但因为最终被解析的是 X(1.0),所以就会抛出 NoSuchMethodError 系统异常。

但换种角度,如果最终解析的是 X(2.0),就没问题了吗?

1、如果 X(2.0) 删掉了 X(1.0) 的一些类,但A已经引用了,同样也会报 NoClassDefFoundError 或者 ClassNotFoundException 错误。

2、如果 X(2.0) 删掉了 X(1.0) 的一些方法,但A已经引用了,同样也会报 NoSuchMethodError 错误。

所以说具体问题还需具体分析,到底采用哪个版本还需要看实际项目。也可能我们需要升级对应的A或者B的版本才能解决问题。

2、Maven 依赖原则

2.1、层级优先原则(路径最近者优先)

在同一个 Pom 内,相同 Jar 不同版本,根据依赖的路径长短来决定引入哪个依赖。

举例

依赖链路一:A -> B -> C -> X(1.0)
依赖链路二:F -> D -> X(2.0)

该例中 X(1.0) 的路径长度为3,而 X(2.0) 的路径长度为2,因此 X(2.0) 会被解析使用。依赖调解第一原则不能解决所有问题,比如这样的依赖关系:

A -> B -> Y(1.0)
c -> D -> Y(2.0)

Y(1.0) 和 Y(2.0) 的依赖路径长度是一样的,都为2。Maven 定义了依赖调解的第二原则:

2.2、声明优先原则(第一声明者优先)

在依赖路径长度相等的前提下,在同一个 Pom 中,间接依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。该例中,如果A的依赖声明在C之前,那么 Y (1.0) 就会被解析使用.

比如 我在 demo01 中引入了 demo02 和 demo03,demo02 和 demo03 都引入了 Lombok 的依赖

图片图片

demo02 和 demo03 换个顺序

图片图片

2.3、特殊情况

  • 子Pom内声明的优先于父 Pom 中的依赖。
  • 同Pom内出现不同版本的相同类库时,后声明的会覆盖先声明的。也就是在同一个Pom里配置了相同资源的不同版本的直接依赖,后配置的覆盖先配置的。比如下边这个例子

图片图片

  • 调换下顺序就是引用的4.12的依赖。

图片图片

3、如何排除依赖

我们先来解释下什么是传递性依赖

3.1、什么是传递性依赖

比如当我们项目中,引用了A的依赖,A的依赖通常又会引入B的 Jar 包,B可能还会引入C的 Jar 包。

这样,当你在 pom.xml 文件中添加了A的依赖,Maven 会自动的帮你把所有相关的依赖都添加进来。

就这样一层层的,Maven 会自动的帮你把所有相关的依赖都添加进来。传递性依赖会给项目引入很多依赖,简化项目依赖管理,但是也会带来问题。

最明显的就是容易发生依赖冲突。

3.2、如何排除依赖

这种情况下,想要解决依赖冲突,可以靠升级/降级某些依赖项的版本,从而让不同依赖引入的同一类库,保持一致的版本号。另外,还可以通过隐藏依赖、或者排除特定的依赖项来解决问题。

3.2.1、<exclusions>标签

Exclusions是主动断开依赖的资源,被排除的资源无需指定版本—指不需要

也就是说可以包含一个或者多 Exclusion 子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 Exclusion 的时候只需要 Groupld 和 Artifactld ,而不需要要 Version 元素。

用法示例:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.1.8.RELEASE</version>
  <exclusions>
    <!-- 排除web包依赖的beans包 -->
    <exclusion>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
    </exclusion>
  </exclusions>
</dependency>

3.2.2、<optional>标签

该标签即是“隐藏依赖”的开关,指对外隐藏当前所依赖的资源---指不透明:

  • true:开启隐藏,当前依赖不会向其他工程传递,只保留给自己用;
  • false:默认值,表示当前依赖会保持传递性,其他引入当前工程的项目会间接依赖。
用法示例:
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.1.8.RELEASE</version>
  <optional>true</optional>
</dependency>

3.2.2.1、上边两种<exclusions>标签和``<optional>标签的区别

  • A依赖B,B依赖C , C 通过依赖传递会被 A 使用到,现在要想办法让 A 不去依赖 C
  • 可选依赖是在B上设置 <optional> , A 不知道有 C 的存在,代表这个依赖是否需要被发现。这种适用于**可以修改B的配置文件的情况下**** 先看默认情况,也就是false

图片图片

  • 改为 true 后

图片图片

  • 排除依赖是在A上设置 <exclusions> , A 知道有 C 的存在,主动将其排除掉。代表这个依赖已经被发现,但自己是否需要引用。这种适用于不能修改B的配置文件的情况下

图片图片

3.2.3、Maven 聚合工程 统一管理版本

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

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

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

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

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

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

4、Maven Helper 插件分析jar包冲突

如果你的项目中依赖许许多多的 Jar ,肉眼排查就没那么方便了,这里推荐一个 Maven 管理插件

图片图片

在 Pom 文件中看到 Dependency Analyzer标志,说明 Maven Helper 插件就安装成功了。

图片图片

点击 Dependency Analyzer 之后就会进入到下面的页面

图片图片

从图中可以看出有哪些jar存在冲突,存在冲突的情况下最终采用了哪个依赖的版本。标红的就是冲突版本,白色的是当前的解析版本。

如果我们想保留标红的版本,那我们可以标白区域右击选择排除(Exclude)即可。

5、总结

一般我们在解决依赖冲突的时候,都会选择保留jar高的版本,因为大部分jar在升级的时候都会做到向下兼容,所以只要保留高的版本就不会有什么问题。

但是有些包,版本变化大没法去做向下兼容,高版本删了低版本的某些类或者某些方法,那么这个时候就不能一股脑的去选择高版本,但也不能选择低版本。

就比如下面这个案例

依赖链路一:A -> B -> C -> X(1.0)
依赖链路二:F -> D -> X(2.0)

X(2.0) 没有对 X(1.0) 做向下兼容也就是说可能存在排除哪个都不行,那怎么办我们只能考虑升级A的版本或者降低F的版本。比如A升级到A(2.0),使它依赖的X版本变成X(2.0)这样的话就解决依赖冲突。

但话有说回来 A升级到A(2.0) 可能会影响许许多多的地方,比如自己项目中代码是否需要改变,或者因为 A升级到A(2.0) 导致 B和C的版本有所改变,这些影响点都需要我们去考虑的。所以说为什么说一个大型项目稳定后,Pom文件的升级是件繁琐的事情,那是因为考虑的东西是在太多了,稍有不慎就会因为依赖冲突而导致系统报错。

责任编辑:武晓燕 来源: 政采云技术
相关推荐

2023-05-09 07:51:28

Spring循环依赖

2024-02-20 21:34:16

循环GolangGo

2021-08-27 07:06:10

IOJava抽象

2023-08-04 08:20:56

DockerfileDocker工具

2023-08-10 08:28:46

网络编程通信

2023-06-30 08:18:51

敏捷开发模式

2022-05-24 08:21:16

数据安全API

2023-09-10 21:42:31

2023-10-26 08:38:43

SQL排名平分分区

2023-05-29 09:07:10

SQLpageSize主键

2022-10-18 07:33:57

Maven构建工具

2022-11-12 12:33:38

CSS预处理器Sass

2022-02-14 07:03:31

网站安全MFA

2022-04-06 08:23:57

指针函数代码

2024-02-26 00:00:00

Go性能工具

2023-07-27 07:46:51

SAFe团队测试

2022-12-07 13:12:15

2022-10-28 07:27:17

Netty异步Future

2022-06-26 09:40:55

Django框架服务

2023-12-28 09:55:08

队列数据结构存储
点赞
收藏

51CTO技术栈公众号