Maven引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖会造成问题。
例如,项目A有这样的依赖关系:X->Y->Z(1.0)、X->G->Z(2.0),Z是X的传递性依赖,但是两条依赖路径上有两个版本的Z,那么哪个Z会被Maven解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。
Maven依赖调解(Dependency Mediation)的第一原则是: 路径最近者优先 。该例中X(1.0)的路径长度为3,而X(2.0)的路径长度为2,因此X(2.0)会被解析使用。
依赖调解第一原则不能解决所有问题,比如上面这个例子,两条依赖路径长度是一样的,都为2。那么到底谁会被解析使用呢?在Maven 2.0.8及之前的版本中,这是不确定的,但是从Maven 2.0.9开始,为了尽可能避免构建的不确定性,Maven定义了依赖调解的第二原则: 第一声明者优先 。
在清楚了Maven的依赖调解规则后,我可以很自然地想到解决方案,就是把我们需要的版本的路径缩短或者声明提前。如下图:
也就是使用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> 复制代码
使用上面三种方法都有一个前提,那就是你选定的version是可以兼容两个冲突的jar。但是两个jar不兼容的话,针对这种情况, 去掉任何一个依赖,都会出现异常。这时,我们查看整个依赖树,找到其父节点,升级其父节点version。
还有一种特殊的冲突,多个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> 复制代码
Maven会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为已解析依赖(Resolved Dependency)。可以运行如下的命令查看当前项目的已解析依赖:
mvn dependency:list 复制代码
能够查看依赖树,通过这棵树能够很清楚的看到某个依赖是通过哪条路径引入进来的。
mvn dependency:tree -Dverbose 复制代码
详细显示依赖信息,把版本冲突中被抛弃,重复的都显示出来,便于排查问题。
mvn dependency:tree -Dverbose --> a.txt 复制代码
将结果保存到文件中
mvn dependence:tree -Dverbose -Dincludes=org.mybatis:mybatis 复制代码
includes指的是想看哪些信息。
参数格式[groupId]:[artifactedId]
可以帮助分析当前项目的依赖。
(1)Used undeclared dependencies
意指项目中使用到的,但是没有显式声明的依赖,这里是spring-context。这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的Java import声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此,显式声明任何项目中直接用到的依赖。
(2)Unused declared dependencies
意指项目中未使用的,但显式声明的依赖,这里有spring-core和spring-beans。需要注意的是,对于这样一类依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。由于dependency:analyze只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。很显然,该例中的spring-core和spring-beans是运行Spring Framework项目必要的类库,因此不应该删除依赖声明。当然,有时候确实能通过该信息找到一些没用的依赖,但一定要小心测试。
首先,需要将下面的插件添加到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 banDuplicateClasses bannedDependencies
当Maven Helper 插件安装成功后,打开项目中的pom文件,下面就会多出一个试图
切换到此试图即可进行相应操作: