gradle的依赖管理和版本冲突的解决
参考文档:
https://www.jianshu.com/p/a132f6a77e7a
https://www.jianshu.com/p/82de510b40b9
https://imperceptiblethoughts.com/shadow/configuration
http://s0docs0gradle0org.icopy.site/current/userguide/userguide.html
一、依赖管理
几乎所有基于 JVM 的软件项目都需要依赖外部的类库来重用现有的功能代码.自动化依赖管理可以明确依赖的版本,能解决传递性依赖带来的版本冲突问题.
而Gradle 就满足这两个条件,以下就来看下依赖管理的关键点.
1.1、项目坐标(jar 包为例)
- group : 指明 jar 包所在的分组
- name : 指明 jar 包的名称
- version: 指明 jar 包的版本
1.1.1、如何定义依赖
- 方式1:依赖一个 project(或者说依赖一个 module)
- 方式2:依赖一个 jar 包
- 方式2:扩展:通过 fileTree 依赖 dir 文件夹下所有的 jar 包
- 方式3:依赖远程仓库
def jdkHome = System.getenv("JAVA_HOME")
dependencies {
//方式1: 依赖一个名字为 "common" 的 project
compile project(":common")
//方式2: 依赖一个本地 jar 包(jdk中lib目录的tools.jar)
compile files("$jdkHome/lib/tools.jar")
//方式2 扩展:通过 fileTree 指定 dir 依赖所有的 jar 包
compile fileTree(dir: 'D:\\Work', include: ['*.jar'])
//方式3: 依赖一个远程仓库的包
compile 'junit:junit:4.12'
}
1.2、仓库(jar 包的存放位置)
- 公共仓库(中央仓库)
Gradle 没有自己的中央仓库,可配置使用 Maven 的中央仓库:mavenCentral/jcenter - 私有仓库
配置从本地 maven 仓库中获取依赖的 jar 包,不远程加载 jar 包,使用 mavenLocal - 自定义 maven 仓库
自定义仓库来源,一般指向公司的 Maven 私服.(普遍做法) - 文件仓库
本地机器上的文件路径,一般不使用,没有意义,因为构建工具的目的就是去除本机的影响,可以在任何地方公用同一份仓库数据,跟本机关联上就没有太大的意义,当然特殊情况下除外.
repositories {
// 优先使用本地maven存储
mavenLocal()
// 然后使用自定义的阿里云maven私服
maven{
url 'https://maven.aliyun.com/repository/public/'
}
// 最后使用maven中央仓库
mavenCentral()
}
1.3、依赖传递性
比如: A 依赖 B,如果 C 依赖 A,那么 C 依赖 B
就是因为依赖的传递性,所以才会出现版本的冲突问题.以下通过一张图来了解下Gradle 的自动化依赖管理流程.
由图可得知,Gradle 工具从远程仓库下载 jar 包到本地仓库,Gradle 工具需要依赖配置文件,如果同一个 jar 经常使用会被存入到依赖缓存中.
1.4、依赖配置
在 build.gradle 中的 dependencies 中配置依赖,依赖分以下四种依赖.
- 源码依赖: compile , runtime
- 测试依赖: testCompile,testRuntime
详细介绍:
- compile 配置的jar,测试代码编译和运行以及源码运行一定存在
- runtime 配置的jar,只有源码运行和测试运行存在
- testCompile 配置依赖的jar,测试代码的源码和运行阶段存在
- testRuntime 配置依赖的jar,只有测试代码的运行存在
关系图如下:
具体效果如下
以上的四种配置选用的主要判断依据是是否仅是运行阶段需要依赖或是否仅是测试阶段需要依赖.runtime ,如果仅是测试阶段需要依赖加 test 前缀 testCompile 或 testRuntime.
1.4.1、Maven中其他配置属性:
熟悉maven 的应该知道,依赖范围控制除了常用的:compile (编译,默认的scope)、runtime (运行时)、test (测试)之外;还有:provided (已提供)、system (系统)、import(导入)等,下面介绍一下相关配置:
-
provided:(编译时依赖,打包时并不打包)
provided 依赖只有在当JDK 或者一个容器已提供该依赖之后才使用。例如, 如果你开发了一个web 应用,你可能在编译 classpath 中需要可用的Servlet API 来编译一个servlet,但是你不会想要在打包好的WAR 中包含这个Servlet API;这个Servlet API JAR 由你的应用服务器或者servlet 容器提供。已提供范围的依赖在编译classpath (不是运行时)可用。它们不是传递性的,也不会被打包。 -
system:(依赖本地文件,systemPath标签指定文件路径)
跟provided 相似,但是在系统中要以外部JAR包的形式提供,maven不会在repository查找它。需通过外部引入,不会在仓库中查找。例如一些特殊的jar我们或通过拷贝jar到web-info/lib下,这些jar就可以配置为system范围。同时必须提供一个systemPath标签,systemPath标签指定文件路径。 -
import:
仅在多个项目的父项目中的 <dependencyManagement> 依赖项上,它表示要在指定的POM <dependencyManagement> 部分中用有效的依赖关系列表替换的依赖关系。该scope类型的依赖项实际上不会参与限制依赖项的可传递性。
依赖传递
当scope的作用域为:test、provided、system 时当前项目依赖JAR并不会被传递到依赖的项目中,其它配置时(compile、runntime)依赖将会传递到依赖的项目中。
例如:有两个项目A、B,当前项目为B,B依赖于A,A项目中配置了dom4j的jar依赖,并将其配置属性设置为:compile,则B项目中会自动依赖A项目的 dom4j,如果配置为:provided,则B项目不会自动添加dom4j 的依赖。
依赖隔断
假设这种情况:A项目的dom4j配置了scope属性为 compile,但是又不想将当前依赖传递到依赖的B项目中,那就要考虑依赖隔断了。
依赖隔断有两种方式:主动隔断和被动隔断,主动隔断:B项目通过配置来进行隔断,被动隔断:A项目配置属性表示当前依赖不传递。
被动隔断
在maven中经常会使用 <optional> true </optional> 参数让依赖只被当前项目使用,而不会在模块间进行传递依赖(A项目配置)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
主动隔断
可以在B项目<dependency>的标签中通过 <exclusion> 子标签来主动的排除当前依赖。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>3.6.3.Final</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
1.4.2、gradle中的其它配置
gradle中除了上面介绍的几种配置之外,还有implementation、testImplementation、compileOnly、testCompileOnly、compileClasspath、testCompileClasspath、runtimeOnly、testRuntimeOnly、runtimeClasspath、testRuntimeClasspath、api(我当时使用的版本未找到当前方法)等
本部分内容参考以下博客,这里只是作归纳和总结使用,如有问题请去原博客查看原文内容
https://www.jianshu.com/p/1a7d933b50aa
https://blog.csdn.net/wangliblog/article/details/81366095
1.4.2.1、implementation和api的区别:
gradle 从 3.4 开始,compile 已经被废弃了,取而代之的是 api | implementation,俩个我们都可以用,但是肯定还是有区别的:
- api - 本地依赖时依赖不隔离,但是编译慢
- implementation - 本地依赖时依赖隔离,编译快
依赖隔离:
假设我本地有四个模块,分别为A、B、C、D, module A 依赖 module B ,module B 又依赖了 module C ,module C 又依赖了 module D;
1. 本地 project 依赖
- (api) A -> D 要是都是 api 的本地依赖方式,那么 A 中可以使用 B,C,D 的 API
- (implementation ) A -> D 要是都是 implementation 的本地依赖方式,那么 A 就只能使用 B 的 API,C,D 的 API 是找不到的
2. 远程 maven 依赖
implementation 就没代码隔离的作用了,B,C,D 的 API A 都能使用
3. 远程和本地依赖混用
若 C -> D 之间是远程依赖,但 A-> B -> C 之间是本地依赖, 使用 implementation 时,B 可以拿到 D 的 api,但是 A 就拿不到 D 的 api 了
编译速度:
在本地依赖时 implementation 可以起到代码隔离的作用,自然编译的速度就快,还是 A-> B -> C -> D 举例,若是 D 更新了,那么只有 C,D 是需要重新编译的。但是一旦我们使用了 API 的依赖方式,那么 ABCD 都需要重新编译
1.4.2.2、compileOnly
compileOnly和provided效果是一样的,只在编译的时候有效, 不参与打包
1.4.2.3、runtimeOnly
runtimeOnly 和 apk效果一样,只在打包的时候有效,编译不参与
1.4.2.4、testImplementation
testImplementation和testCompile效果一样,在单元测试和打包测试apk的时候有效
1.5、打包时将依赖也合并入jar包
有些情形需要将项目依赖的jar包也合并入自己项目的jar包内,出来的这个jar我们称它为fat-jar。
举个例子:我编写了一个 m3u8格式下载的jar工程,最后打包成了一个jar,想要通过 java -jar 的形式启动,其中项目使用到了 HttpClient等,需要将其打包到项目的jar文件中。
方式一:自定义task来搞定
有部分博客说这种打包方式有问题,但是本人测试发现一切正常(使用gradle版本为:6.2.2)
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
// 设置编码格式
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
repositories {
// 优先使用本地maven存储
mavenLocal()
// 然后使用自定义的阿里云maven私服
maven{
url 'https://maven.aliyun.com/repository/public/'
}
// 最后使用maven中央仓库
mavenCentral()
}
dependencies {
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.8'
compile 'commons-lang:commons-lang:2.6'
compile 'commons-io:commons-io:2.4'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
// 执行当前task则可以将依赖也合并入jar包
task customFatJar(type: Jar) {
// 设置运行的主类信息
manifest {
attributes 'Main-Class': 'com.milory.download.DownLoad'
}
// 打包文件的名字
archiveName = 'm3u8Down.jar'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
方式二:使用shadow插件来搞定
注意 shadowJar 中的 relocate 配置只是为了演示一下插件的强大功能,有没有此配置都可以实现上述的功能
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '6.0.0'
}
group 'org.example'
version '1.0-SNAPSHOT'
// 设置编码格式
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
repositories {
// 优先使用本地maven存储
mavenLocal()
// 然后使用自定义的阿里云maven私服
maven{
url 'https://maven.aliyun.com/repository/public/'
}
// 最后使用maven中央仓库
mavenCentral()
}
dependencies {
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.8'
compile 'commons-lang:commons-lang:2.6'
compile 'commons-io:commons-io:2.4'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
//jar {
// manifest {
// attributes 'Main-Class': 'com.milory.download.DownLoad'
// }
//}
// 从标准任务清单jar中继承配置,所以这里的 manifest 属性可以直接写再外面的jar配置中,也可以写再 shadowJar 内部
shadowJar {
baseName = "abc"
// 并将包名由 com.milory.download 改成 org.milory.download
relocate("com.milory.download", "org.milory.download")
manifest {
attributes 'Main-Class': 'org.milory.download.DownLoad'
}
}
其实上面的配置可以简写成以下的形式,上面的功能中
- baseName 配置的是打包后文件的前缀名,这个属性不配置的情况下也可
- relocate 在合并包的时候修改依赖的包名(这个为演示功能)
- manifest 中配置的运行主类信息,这个可以直接在 jar 中进行集成
综合了上面所述,配置可简化如下:
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '6.0.0'
}
group 'org.example'
version '1.0-SNAPSHOT'
// 设置编码格式
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
repositories {
// 优先使用本地maven存储
mavenLocal()
// 然后使用自定义的阿里云maven私服
maven{
url 'https://maven.aliyun.com/repository/public/'
}
// 最后使用maven中央仓库
mavenCentral()
}
dependencies {
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.8'
compile 'commons-lang:commons-lang:2.6'
compile 'commons-io:commons-io:2.4'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
jar {
manifest {
attributes 'Main-Class': 'com.milory.download.DownLoad'
}
}
打包的时候,运行 shadowJar 任务即可
两次打包成功后的文件名称如下:
二、版本冲突问题
之前我们介绍了如何添加依赖,接下来咱们来看下如何来管理依赖.其实管理依赖的最重要的环节就是传递性依赖过程中存在的版本冲突的问题处理.接下来咱们先来看个存在传递性依赖过程中 jar 测版本冲突问题,再来看下 Gradle 有哪些的处理方法.
2.1、概念引入
上图咱们可以知道,咱们需要依赖 hibernate3.6.3 的版本,而此版本需要依赖几个 jar 包,而依赖的 hibernate-commons-annotations.jar 依赖了一个slf4j 的jar包,当前hibernate框架也依赖了slf4j 的 jar 包.由于传递性依赖的特点,两个不同版本的 jar 包会被依赖进来,这样就存在版本冲突的问题.
2.2、版本冲突解决方法
对于 Maven 的自动处理传递性依赖版本冲突问题,是按最短路径和优先声明原则来处理.
而对于 Gradle 来说同样有着自动处理传递性依赖版本冲突问题的功能,只是 Gradle 是默认使用版本最高的.而针对一些特殊的需求还是需要使用手动解决.以下便是 Gradle 的手动处理版本冲突.
2.2.1、修改默认配置策略
如果想检查有哪些 jar 包有版本冲突,或者想去除 Gradle 的默认处理方式,可修改默认的配置策略.
在 build.gradle 中加入如下配置
plugins {
id 'java'
}
// 配置gradle版本冲突的解决策略
configurations.all{
resolutionStrategy{
// 修改 gradle不自动处理版本冲突
failOnVersionConflict()
}
}
group 'com.milory'
version '1.0-SNAPSHOT'
repositories {
mavenLocal()
maven{
url 'https://maven.aliyun.com/repository/public/'
}
mavenCentral()
}
// 添加 hibernate的依赖,用于演示版本冲突问题
dependencies {
compile group: 'org.hibernate', name: 'hibernate-core', version: '3.6.3.Final'
}
执行 help -> dependencies 可查看冲突的 jar 包
> Conflict(s) found for the following module(s):
- org.slf4j:slf4j-api between versions 1.6.1 and 1.5.8
Run with:
--scan or
:dependencyInsight --configuration compileClasspath --dependency org.slf4j:slf4j-api
to get more insight on how to solve the conflict.
可以看到以上配置完,如果存在依赖 jar 包版本冲突问题, Gradle 将不再自动处理,build 会抛异常
2.2.2、排除传递性依赖
排除单个jar 包的传递性依赖
dependencies {
compile(group: 'org.hibernate', name: 'hibernate-core', version: '3.6.3.Final'){
// 排除掉hibernate-core 中依赖的 org.slf4j 依赖
exclude(group:"org.slf4j" , module:"slf4j-api")
}
// 排除掉依赖后我们手动添加合适的 org.slf4j 版本
compile "org.slf4j:slf4j-api:1.6.1"
}
此处的 module 指的是 jar 的 name,值无需写版本,此配置的意义就是排除 hibernate-core 引入时 slf4f-api 的传递性依赖.也就是说咱们的项目目前没有依赖任何版本的 slf4f-api,所以需要单独引入合适版本的。
排除所有 jar 的传递性依赖
dependencies {
compile (group: 'org.hibernate', name: 'hibernate-core', version: '3.6.3.Final'){
transitive=false
}
}
transitive 默认为true ,表示 Gradle 自动添加子依赖项,设置为false 则需要手动添加每个依赖项,在真实开发中,需要自动添加子依赖的比较多,而手动添加的比较少,毕竟使用工具的主要目的是减少工作量嘛,所以开发中此配置建议不使用.
2.2.3、强制指定一个版本
给有冲突的 jar 包强制指定一个版本,在 build.gradle 中配置如下
configurations.all{
resolutionStrategy{
force 'org.slf4j:slf4j-api:1.7.24'
}
}
执行 help -> dependencies 可看到所有的 slf4j 都改为了1.7.24 的版本了.
> Task :dependencies
------------------------------------------------------------
Root project
------------------------------------------------------------
annotationProcessor - Annotation processors and their dependencies for source set 'main'.
No dependencies
apiElements - API elements for main. (n)
No dependencies
archives - Configuration for archive artifacts.
No dependencies
compileClasspath - Compile classpath for source set 'main'.
\--- org.hibernate:hibernate-core:3.6.3.Final
+--- antlr:antlr:2.7.6
+--- commons-collections:commons-collections:3.1
+--- dom4j:dom4j:1.6.1
+--- org.hibernate:hibernate-commons-annotations:3.2.0.Final
| \--- org.slf4j:slf4j-api:1.5.8 -> 1.7.24
+--- org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.0.Final
+--- javax.transaction:jta:1.1
\--- org.slf4j:slf4j-api:1.6.1 -> 1.7.24