Maven依赖冲突
在 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>