1 什么是传递依赖冲突
Maven 引入的传递性依赖机制, 一方面大大简化和方便了依赖声明, 另一方面, 大部分情况下我们只需要关心项目的直接依赖是什么, 而不用考虑这些直接依赖会引入什么传递性依赖. 但有时候, 当传递性依赖会造成问题.
例如, 项目 A 有这样的依赖关系: X->Y->Z(1.0),X->G->Z(2.0),Z 是 X 的传递性依赖, 但是两条依赖路径上有两个版本的 Z, 那么哪个 Z 会被 Maven 解析使用呢? 两个版本都被解析显然是不对的, 因为那会造成依赖重复, 因此必须选择一个.
2 你必须知道的依赖调解规则
Maven 依赖调解 (Dependency Mediation) 的第一原则是: 路径最近者优先. 该例中 X(1.0)的路径长度为 3, 而 X(2.0)的路径长度为 2, 因此 X(2.0)会被解析使用.
依赖调解第一原则不能解决所有问题, 比如上面这个例子, 两条依赖路径长度是一样的, 都为 2. 那么到底谁会被解析使用呢? 在 Maven 2.0.8 及之前的版本中, 这是不确定的, 但是从 Maven 2.0.9 开始, 为了尽可能避免构建的不确定性, Maven 定义了依赖调解的第二原则: 第一声明者优先.
3 如何解决依赖冲突
3.1 加载提前
在清楚了 Maven 的依赖调解规则后, 我可以很自然地想到解决方案, 就是把我们需要的版本的路径缩短或者声明提前. 如下图:
3.2 排除依赖
也就是使用 exclusions 元素声明排除其中一个依赖, exclusions 可以包含一个或者多个 exclusion 子元素, 因此可以排除一个或者多个传递性依赖. 需要注意的是, 声明 exclusion 的时候只需要 groupId 和 artifactId, 而不需要 version 元素, 这是因为只需要 groupId 和 artifactId 就能唯一定位依赖图中的某个依赖. 换句话说, Maven 解析后的依赖中, 不可能出现 groupId 和 artifactId 相同, 但是 version 不同的两个依赖.
- <dependency>
- <groupId>com.alibaba.lava</groupId>
- <artifactId>lava-core</artifactId>
- <version>3.0.1-YUJUN-SNAPSHOT</version>
- <exclusions>
- <exclusion>
- <groupId>org.mybatis</groupId>
- <artifactId>*</artifactId>
- </exclusion>
- <exclusion>
- <artifactId>jakarta.commons.collections</artifactId>
- <groupId>com.alibaba.external</groupId>
- </exclusion>
- <exclusion>
- <artifactId>mcms.client</artifactId>
- <groupId>com.alibaba.intl.sourcing.shared</groupId>
- </exclusion>
- </exclusions>
- </dependency>
3.3 升级父节点
使用上面三种方法都有一个前提, 那就是你选定的 version 是可以兼容两个冲突的 jar. 但是两个 jar 不兼容的话, 针对这种情况, 去掉任何一个依赖, 都会出现异常. 这时, 我们查看整个依赖树, 找到其父节点, 升级其父节点 version.
4 全路径冲突
还有一种特殊的冲突, 多个 dependency 的 groupID 或 artifactID 不同(或两者都不同), 但包中存在全路径类名相同的类 Java 类加载器根据 classpath 加载类时, 根据 classpath 中 jar 包出现的先后顺序进行查找类并缓存, 后面 jar 包中的类不使用. 这个时候的常见异常就是 NoSuchMethodException,NoClassDefFoundError,ClassNotFoundException,NoSuchMethodError 等.
如果其中一个 jar 是我们不需要的, 那么排除它就行了. 但是, 如果这个 jar 被很多 dependency 依赖, 你需要一个个去写 exclusions 是不是很麻烦. 这时我们可以直接在 pom 中添加一个空依赖(和想要去掉的 jar 的 groupID,artifactID 相同, 但是 version 不同的一个空项目打包上传到远程仓库中).
- <!-- ================================================= -->
- <!-- 排除依赖 -->
- <!-- ================================================= -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-nop</artifactId>
- <version>999-not-exist-v3</version>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- <version>999-not-exist-v3</version>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>log4j-over-slf4j</artifactId>
- <version>999-not-exist</version>
- </dependency>
5 如何快速发现依赖冲突
5.1 常用的 Maven 命令
5.1.1 mvn dependency:list
Maven 会自动解析所有项目的直接依赖和传递性依赖, 并且根据规则正确判断每个依赖的范围, 对于一些依赖冲突, 也能进行调节, 以确保任何一个构件只有唯一的版本在依赖中存在. 在这些工作之后, 最后得到的那些依赖被称为已解析依赖(Resolved Dependency). 可以运行如下的命令查看当前项目的已解析依赖:
mvn dependency:list
5.1.2 mvn dependency:tree
能够查看依赖树, 通过这棵树能够很清楚的看到某个依赖是通过哪条路径引入进来的.
mvn dependency:tree -Dverbose
详细显示依赖信息, 把版本冲突中被抛弃, 重复的都显示出来, 便于排查问题.
mvn dependency:tree -Dverbose --> a.txt
将结果保存到文件中
mvn dependence:tree -Dverbose -Dincludes=org.mybatis:mybatis
includes 指的是想看哪些信息.
参数格式[groupId]:[artifactedId]
5.1.3 mvn dependency:analyze
可以帮助分析当前项目的依赖.
(1)Used undeclared dependencies
意指项目中使用到的, 但是没有显式声明的依赖, 这里是 spring-context. 这种依赖意味着潜在的风险, 当前项目直接在使用它们, 例如有很多相关的 Java import 声明, 而这种依赖是通过直接依赖传递进来的, 当升级直接依赖的时候, 相关传递性依赖的版本也可能发生变化, 这种变化不易察觉, 但是有可能导致当前项目出错. 例如由于接口的改变, 当前项目中的相关代码无法编译. 这种隐藏的, 潜在的威胁一旦出现, 就往往需要耗费大量的时间来查明真相. 因此, 显式声明任何项目中直接用到的依赖.
(2)Unused declared dependencies
意指项目中未使用的, 但显式声明的依赖, 这里有 spring-core 和 spring-beans. 需要注意的是, 对于这样一类依赖, 我们不应该简单地直接删除其声明, 而是应该仔细分析. 由于 dependency:analyze 只会分析编译主代码和测试代码需要用到的依赖, 一些执行测试和运行时需要的依赖它就发现不了. 很显然, 该例中的 spring-core 和 spring-beans 是运行 Spring Framework 项目必要的类库, 因此不应该删除依赖声明. 当然, 有时候确实能通过该信息找到一些没用的依赖, 但一定要小心测试.
5.1.4 mvn enforcer:enforce
首先, 需要将下面的插件添加到 pom.xml 中:
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-enforcer-plugin</artifactId>
- <version>1.4.1</version>
- <configuration>
- <rules><dependencyConvergence/></rules>
- </configuration>
- </plugin>
- </plugins>
注意这里配置的 rules 规则, 它有很多内置的规则:
dependencyConvergence
: 确保依赖只有一个版本存在于项目中, 否则 build 报错, 报错信息中就告诉你哪些有冲突了.
banDuplicateClasses
: 找到重复的类.
bannedDependencies: 配置不允许使用的 jar. 例如只能使用 log4j2, 不允许使用 log4j1.
5.2 IDEA maven helper 插件
当 Maven Helper 插件安装成功后, 打开项目中的 pom 文件, 下面就会多出一个试图
切换到此试图即可进行相应操作:
- Conflicts(查看冲突)
- All Dependencies as List(列表形式查看所有依赖)
- All Dependencies as Tree(树形式查看所有依赖)