Maven依赖冲突

更新于:2020-11-16     浏览:1780 次  

在 Maven 中,依赖分为直接依赖和传递依赖(即间接依赖)。


直接依赖,顾名思义,无须赘述。对比之下,传递依赖是 Maven 的特色和重点,可大书特书。


Maven 引入的传递性依赖机制,能大大简化依赖管理,因为大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。虽然 Maven 的传递依赖机制给项目构建提供了极大的便利,但是却暗藏隐患,因为依赖冲突令人抓狂。


Maven直接依赖冲突

众所周知,对于 Maven 而言,同一个 groupId 同一个 artifactId 下,只能使用一个 version!

<dependencies>

	<dependency>
		<groupId>org.mybatis</groupId>
		<artifactId>mybatis</artifactId>
		<version>3.3.0</version>
	</dependency>

	<dependency>
		<groupId>org.mybatis</groupId>
		<artifactId>mybatis</artifactId>
		<version>3.5.0</version>
	</dependency>

</dependencies>

根据上列的依赖顺序,项目将使用 3.5.0 版本的 MyBatis Jar。


现在,我们可以思考下,比如工程中需要引入A、B,而 A 依赖 1.0 版本的 C,B 依赖 2.0 版本的 C,那么问题来了,C 使用的版本将由引入A、B的顺序而定? 这显然不靠谱!如果 A 的依赖写在 B 的依赖后面,将意味着最后引入的是 1.0 版本的 C,很可能在运行阶段出现类(ClassNotFoundException)、方法(NoSuchMethodError)找不到的错误(因为B使用的是高版本的C)!


Maven传递依赖冲突

Maven 引入的传递性依赖机制,能大大简化依赖管理,因为大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但是却暗藏隐患,因为依赖冲突令人抓狂。


依赖传递的发生有两种情况:一种是存在模块之间的继承关系,在继承父模块后同时引入了父模块中的依赖,可通过可选依赖机制放弃依赖传递到子模块;另一种是引包时附带引入该包所依赖的包,该方式是引起依赖冲突的主因。如下例所示:



例如,项目 A 有这样的依赖关系:X->Y->Z(1.0)、X->G->Z(2.0),Z 是 X 的传递性依赖,但是两条依赖路径上有两个版本的 Z,那么哪个 Z 会被 Maven 解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。


Maven 依赖优化

实际上 Maven 是比较智能的,它能够自动解析直接依赖和传递性依赖,根据预定义规则判断依赖范围的合理性,也可以对部分依赖进行适当调整来保证构件版本唯一。


即使这样,还会有些情况使 Maven 发生误判,因此手工进行依赖优化还是相当有必要的。我们可以使用 maven-dependency-plugin 提供的三个目标来实现依赖分析:


$ mvn dependency:list
$ mvn dependency:tree
$ mvn dependency:analyze

如若需更精细的分析结果,可以在命令后使用诸如以下参数:


-Dverbose

-Dincludes=<groupId>:<artifactId>

Maven依赖冲突调解规则

有冲突必然有调节,总有好事佬喜欢充当调节员。调节者的立场和原则非常重要,这样才能做到公平和公正。


软件开发世界也不例外,面对Maven依赖冲突问题,有四种原则:

路径近者优先原则,第一声明者优先原则,排除原则和版本锁定原则。


依赖调解第一原则不能解决所有问题,比如上面这个例子,两条依赖路径长度是一样的,都为2。那么到底谁会被解析使用呢?在Maven 2.0.8及之前的版本中,这是不确定的,但是从Maven 2.0.9开始,为了尽可能避免构建的不确定性,Maven定义了依赖调解的第二原则:第一声明者优先。


Maven依赖调解详解

Maven 依赖调解遵循以下两大原则:路径最短优先、声明顺序优先


第一原则:路径最近者优先


把当前模块当作顶层模块,直接依赖的包则作为次层模块,间接依赖的包则作为次层模块的次层模块,依次递推...,最后构成一棵引用依赖树。假设当前模块是A,两种依赖路径如下所示:


A --> B --> X(1.1)         // dist(A->X) = 2

A --> C --> D --> X(1.0)   // dist(A->X) = 3

此时,Maven可以按照第一原则自动调解依赖,结果是使用X(1.1)作为依赖。


第二原则:第一声明者优先


若冲突依赖的路径长度相同,那么第一原则就无法起作用了。假设当前模块是A,两种依赖路径如下所示:


A --> B --> X(1.1)   // dist(A->X) = 2

A --> C --> X(1.0)   // dist(A->X) = 2

当路径长度相同,则需要根据A直接依赖包在pom.xml文件中的先后顺序来判定使用那条依赖路径,如果次级模块相同则向下级模块推,直至可以判断先后位置为止。



<!-- A pom.xml -->
<dependencies>
    ...
    dependency B
    ...
    dependency C
</dependencies>

假设依赖B位置在依赖C之前,则最终会选择X(1.1)依赖。


其它情况:覆盖策略


若相同类型但版本不同的依赖存在于同一个 pom 文件,依赖调解两大原则都不起作用,需要采用覆盖策略来调解依赖冲突,最终会引入最后一个声明的依赖。如下所示:



<!-- 该pom文件最终引入commons-cli:commons-cli:1.3.jar依赖包。 -->

<dependencies>

  <dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.2</version>
  </dependency>

  <dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.4</version>
  </dependency>

  <dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.3</version>
  </dependency>

</dependencies>

Maven 解决依赖冲突

冲突解决方式简单粗暴,直接在 pom.xml 文件中排除冲突依赖即可。


<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-grizzly2-http</artifactId>
  <!-- 剔除依赖 -->
  <exclusions>
    <exclusion>
      <groupId>org.glassfish.hk2.external</groupId>
      <artifactId>jakarta.inject</artifactId>
    </exclusion>
    ...
  </exclusions>
</dependency>