Maven依赖冲突问题

Java
302
0
0
2024-05-27
标签   Maven

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),所以就会出现很典型的 NoClassDefFoundErrorClassNotFoundException 依赖冲突报错。

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文件的升级是件繁琐的事情,那是因为考虑的东西是在太多了,稍有不慎就会因为依赖冲突而导致系统报错。