一、背景
在java项目中有maven,gradle这样的项目构建工具,可以通过gradle和maven的命令来快速对于项目进行编译,构建,以及项目所需要依赖的管理。
在实际工作中随着项目越来越大,引入依赖越来越多,依赖之间版本冲突无法避免,那么对于这种冲突我们应该怎么处理以确保项目使用正常的依赖版本呢。
思考:项目引入A,B两个模块,两个模块分别引入了abd.jar。A模块使用的1.0版本,B模块使用的2.0版本。那么最终项目构建完成,会使用1.0版本还是2.0版本呢?
二、默认管理办法
针对上面的问题,在不同的构建工具中使用了不同的管理策略。在gradle中选择使用高版本覆盖低版本。但是在maven中选择的是就近原则
,也就是谁的依赖路径更短,就选择依赖路径相对较短的版本作为最终版版。
- Gradle:更高版本优先
- Maven:依赖路径更近优先
对于gradle的策略比较容易理解,对于maven的更短依赖路径应该怎么理解呢?我们通过下面的例子来看两者的不同。
现在有一个依赖包
com.google.guava:guava
,有不同的两个版本分别是20.0
and25.1-android
。
- 项目直接依赖了
com.google.guava:guava:20.0
- 项目也依赖了
com.google.inject:guice:4.2.2
,guice包依赖com.google.guava:guava:25.1-android
在这个场景下按照我们上面所说的:
- gradle构建的项目最终会选择
25.1-android
版本,因为版本更高。 - maven构建的项目会选择20.0,因为从依赖路径来看,20.0版本只有一级路径,25.1-android版本需要两级引入路径。
依赖查看命令
在maven和gradle中也提供了命令可以快速查看项目的引入依赖情况。
- mvn dependency:tree
- gradlew dependencies
对于gradle而言如果要单独看某一个jar的依赖可以执行dependencyInsight命令。例如:gradlew dependencyInsight --dependency srn-library单独查看srn-library包的依赖。
以下是gradle dependencies命令的示例输出。
shell
./gradlew :gateway:dependencies
> Task :gateway:dependencies
------------------------------------------------------------
Project ':gateway'
------------------------------------------------------------
annotationProcessor - Annotation processors and their dependencies for source set 'main'.
--- org.projectlombok:lombok:1.18.4
apiElements - API elements for main. (n)
No dependencies
archives - Configuration for archive artifacts. (n)
No dependencies
bootArchives - Configuration for Spring Boot archive artifacts. (n)
No dependencies
compileClasspath - Compile classpath for source set 'main'.
+--- com.nimbusds:nimbus-jose-jwt:6.0.2
| +--- com.github.stephenc.jcip:jcip-annotations:1.0-1
| --- net.minidev:json-smart:[1.3.1,2.3] -> 2.4.8
| --- net.minidev:accessors-smart:2.4.8
| --- org.ow2.asm:asm:9.1
+--- org.projectlombok:lombok:1.18.4
+--- project :gateway:mbg
+--- com.anet.ops:base:1.5
| +--- com.vip.vjtools:vjkit:1.0.0
| | +--- com.google.guava:guava:20.0 -> 23.0
| | | +--- com.google.code.findbugs:jsr305:1.3.9 -> 3.0.2
| | | +--- com.google.errorprone:error_prone_annotations:2.0.18 -> 2.10.0
| | | +--- com.google.j2objc:j2objc-annotations:1.1
| | | --- org.codehaus.mojo:animal-sniffer-annotations:1.14
| | +--- org.apache.commons:commons-lang3:3.7 -> 3.8.1
| | +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.36
| | +--- net.sf.dozer:dozer:5.5.1
| | | +--- commons-beanutils:commons-beanutils:1.9.1
| | | | --- commons-collections:commons-collections:3.2.1
| | | +--- org.apache.commons:commons-lang3:3.2.1 -> 3.8.1
| | | +--- org.slf4j:slf4j-api:1.7.5 -> 1.7.36
| | | --- org.slf4j:jcl-over-slf4j:1.7.5 -> 1.7.36
| | | --- org.slf4j:slf4j-api:1.7.36
| | +--- junit:junit:4.12 -> 4.13.2
| | | --- org.hamcrest:hamcrest-core:1.3 -> 2.2
| | | --- org.hamcrest:hamcrest:2.2
| | +--- org.assertj:assertj-core:2.6.0 -> 3.22.0
| | --- org.mockito:mockito-core:1.10.19 -> 4.5.1
| | +--- net.bytebuddy:byte-buddy:1.12.9 -> 1.12.13
....
三、管理策略
在了解了默认依赖管理策略之后,我们知道了gradle会选择最新的版本保留依赖,可是并不是所有时候我们都希望这么做。有时候A,B两个模块有共同依赖jar abd之后,我们就是希望A模块中引入的依赖可以保留低版本,而B模块中的依赖保持高版本。
如果我们要特别设定某个模块使用特定的依赖,或者要求整个项目使用特定版本的依赖,我们可以使用force或者exclude等特性来处理。
Gradle中支持多种策略来自定义依赖解析,这里为大家描述几个日常使用的。基本上以下几个用法就能满足日常需求,如果想要了解更多可以阅读官方自定义依赖解析。
exclude
可以通过exclude来指定特定的jar或者模块中,被exclude排除的依赖不引入到项目来,从而避免和当前项目中其他模块相同的依赖产生冲突。
shell
dependencies {
implementation('com.example:library:1.0.0') {
// 排除名为 'unwanted-dependency' 的传递性依赖项
exclude group: 'unwanted-group', module: 'unwanted-dependency'
}
}
在这个示例中,implementation
声明中的 exclude
方法指定了要排除的传递性依赖项的组和模块名称。这样,Gradle 将会在解析依赖关系时会忽略指定的依赖项,从而确保它们不会被引入到项目中,避免和其他模块相同的依赖产生冲突。
force
如下代码所示,项目最终会强制使用17.5.0版本所有依赖版本,这样的调整范围是整个项目的,不管哪个依赖版本高低,项目最终都使用force指定的版本。
shell
configurations.all {
resolutionStrategy {
force 'com.google.firebase:firebase-analytics:17.5.0'
}
}
dependencyManagement
在maven和gradle中都可以使用dependencymanagement来统一指定版本。
shell
<!-- 父模块的pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8</version> <!-- 指定统一版本 -->
</dependency>
</dependencies>
</dependencyManagement>
四、特殊情况
使用gradle作为构建工具
如果依赖jar包是通过implementation引入项目的,那么在编辑阶段可能会出现低版本没有被覆盖的情况。直接在idea或者编辑器里面直接运行项目,可能会引发这种情况。
这是因为gradle对于implementation和api的差异导致的,通过implementation引入的依赖在编译阶段只会对当前模块生效不会传递给其他模块。
详情参考文章记录一次服务依赖处理冲突 - 掘金 (juejin.cn)