【Gradle】(三)详细聊聊依赖管理:坐标、依赖配置、依赖传递、依赖冲突

文章目录

1.概述

在前面两篇文章中,我们已通过 Gradle 创建了一个多模块的项目,并介绍了一些常用的核心概念。至此,我们已能够在工作中使用 Gradle 了。

本篇内容主要讲解 Gradle 的依赖管理,旨在让大家对 Gradle 的依赖特性有一定了解,能够处理工作中出现的一系列依赖冲突问题,包含以下内容:

  • 讲解坐标的概念
  • 介绍依赖范围的概念及选择
  • 说明依赖如何传递、如何避免依赖传递
  • 阐述依赖冲突的概念、如何确认依赖冲突、如何解决依赖冲突

Gradle历史文章:
《【Gradle】(一)在IDEA中初始化gradle项目及其核心概念解释》
《【Gradle】(二)多模块项目、api推送到Maven私服、及SpringBoot可执行Jar包》


Maven相关文章:
《Maven中的依赖功能特性》
《Maven坐标的概念与规则》

对Maven感兴趣的话,可以看看我的上面两篇Maven文章,里面也介绍了一些依赖的概念,当然,如果不感兴趣的话,本篇文章也会引用这两篇Maven中的部分内容,不影响本篇文章的阅读。

2.依赖管理

2.1.坐标

在 Gradle 中,依赖的版本控制通常基于 Maven 的坐标系统,使用组 ID(groupId)、构件 ID(artifactId)和版本号(version)来唯一标识一个依赖项,这被称为 GAV(坐标)。

例如:implementation "cn.hutool:hutool-all:5.8.18" ,其中 cn.hutool 是组 ID,hutol-all是构件 ID,5.8.18 是版本号。

Gradle的构建一般也是使用的Maven仓库,很多时候在仓库中的jar包是共用的,所以我们在编写坐标时,也应遵循Maven的规范:

  • groupId:指的是当前构建隶属的实际项目,一般是 公司的网址倒序 + 项目名
  • artifactId:一般是指的当前项目中的其中一个模块
  • version:当前项目的版本号

版本号也不是随便写的,在业界有通用的规则,定义方式如下:

{主版本号}.{次版本号}.{增量版本号}-{里程碑版本}
  • 主版本号:一般是指的当前的项目有了重大的架构变动,版本之间几乎完全不兼容,例如:最近出的 SpringBoot3 就已经放弃了Java8,如果不升级 JDK的话,还是只能使用SpringBoot2
  • 次版本号:一般是指的项目的迭代版本,这种版本会修复大量的bug,带来一些小的新特性等,但是没有什么架构上的重大变化。
  • 增量版本号:一般是用于修复一些紧急bug,改动量很小时,可以使用增量版本号。
  • 里程碑版本 :就是项目的当前版本处于一个什么样的阶段了,常见的里程碑版本有 SNAPSHOTalphabetareleaseGA 等。
    在里程碑的版本中,标注SNAPSHOT的为开发版,此时会存在大量的代码变动,alphabeta分别对应的是内测版与公测版这三个版本都是属于不稳定版本 ,使用的时候非常容易踩坑,所以一般只用的demo体验,在正式环境中不能使用。
    releaseGA都属于是稳定的正式版本,可以在正式环境中使用。

下面是SpringBoot的一些版本号,可以体验一下:

2.2.依赖的基本概念

在软件开发中,依赖是指一个项目或模块对其他项目或模块的需求。例如,如果一个项目需要使用某个第三方库来实现某些功能,那么这个项目就对该第三方库存在依赖。

假设现在有 A、B、C 三个构件,如下图中表示的就是A依赖B和C

通过坐标定义:

groovy 复制代码
dependencies {
    implementation "com.ls:B:1.0.0"
    implementation "com.ls:C:1.0.0"
}

2.3.依赖配置(Dependency configurations)

在这个章节会了解到Gradle中的几种常用的依赖路径,以及依赖配置和依赖范围的对应关系《官方参考文档》

2.3.1.依赖路径

从宏观的角度来看,常用的依赖类型有两种:编译路径(CompileClasspath)运行时路径(RuntimeClasspath),在这两个概念的基础上,又加入了测试代码和非测试代码的区分,概念如下:

  • CompileClasspath:编译类路径,在编译项目时需要的依赖路径。
  • RuntimeClasspath:运行时类路径,在运行项目时需要的依赖路径。
  • TestCompileClasspath:测试代码编译类路径,是在编译测试代码时所需的依赖路径。
  • TestRuntimeClasspath:测试代码运行时类路径,用于运行测试代码时所需的依赖路径。

除此之外,还有一种特殊的路径annotationProcessor,它也是一种依赖配置,在编译时生效。主要是用于将某些注解添加到依赖路径中,启用对注解的支持。在上篇文章中提到的lombok的注解支持就是这种类型:

groovy 复制代码
dependencies {
    compileOnly "org.projectlombok:lombok:$lombokVersion"
    annotationProcessor "org.projectlombok:lombok:$lombokVersion"
}

说的更直白一点,所谓的编译时路径,就是写代码的时候可以直接Import使用的路径,运行时路径就是写代码的时候无法引入,但在程序运行的时候能够访问到(常见于间接依赖中)。

2.3.2.依赖配置与依赖路径的关联

  • implementation:添加直接依赖包到编译时和运行时环境
  • api:添加直接依赖包和间接依赖包到编译时和运行时环境,即向上层传递内部依赖关系
  • compileOnly:添加依赖包,不添加到运行时路径
  • runtimeOnly :不添加到编译时路径,添加运行时路径
  • annotationProcessor:添加到注解处理器的类路径,不添加到运行时路径

更多依赖配置对应的依赖路径关系如下表所示:

依赖配置 Compile Classpath Runtime Classpath
api
implementation
compileOnlyApi ×
compileOnly ×
runtimeOnly ×
testImplementation
testCompileOnly ×
testRuntimeOnly ×
annotationProcessor ×

在上面的表格中,有test前缀的依赖配置,只会在test任务中生效。

此外,可以看到还多了一个api,这也是一个可能会使用到的功能,主要是用在依赖传递中,在下面会讲解。

2.4.依赖传递

我们上面提到了编译时运行时 两种依赖路径,依赖传递也分为编译时依赖传递运行时依赖传递 ,其中编译时依赖传递还依赖于api这个依赖配置,要使用这个配置,还需要额外引入java-library插件。下面就通过一个demo案例来体验一下两种依赖传递,同时,会介绍IDEA中自带的一种依赖查看工具 Dependency Analyzer

2.4.1.准备工作

需要创建3个子模块进行模拟,分别是a-demo,b-demo,c-demo,同时需要将它们打包到本地Maven仓库中。

创建子模块:

创建3个子模块,并修改每个子模块的build.gradle,里面只放入version = 1.0.0,其他配置由根项目进行配置。

根项目配置

根项目中需要配置的有三个,插件配置仓库配置发布配置,配置的含义在本系列的前两篇文章中已经讲过了,这里就不再赘述了,根项目完整的配置如下,

groovy 复制代码
plugins {
    id 'java'
    id 'org.springframework.boot' version "$springBootVersion" apply false
    id 'io.spring.dependency-management' version "$springDenpendencyVersion" apply false
}

allprojects {
    repositories {
        mavenLocal()
        maven {
            url 'https://maven.aliyun.com/repository/public'
        }
        mavenCentral()
    }
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'maven-publish'
    apply plugin: 'java-library'

    sourceCompatibility = JavaVersion.VERSION_1_8

    java {
        withSourcesJar()
    }

    publishing {
        publications {
            demo(MavenPublication) {
                from components.java
            }
        }

        repositories {
            maven {
                name = "mavenNexus"
                allowInsecureProtocol = true
                def releasesRepoUrl = 'http://192.168.200.101:8081/repository/maven-releases/'
                def snapshotsRepoUrl = 'http://192.168.200.101:8081/repository/maven-snapshots/'
                url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
                credentials {
                    username 'admin'
                    password 'admin123'
                }
            }
        }
    }

    dependencies {
        compileOnly "org.projectlombok:lombok:$lombokVersion"
        annotationProcessor "org.projectlombok:lombok:$lombokVersion"
    }

}

gradle.properties配置

资源文件中需要配置项目整体的groupId和默认的version。

js 复制代码
group=com.ls
version=1.0.0

2.4.2.运行时依赖传递

jar包生成与依赖配置

要感受到依赖传递,需要依次对c-demob-demo 两个依赖打包,首先是对c进行打包和推送到本地Maven仓库。

推送完成后修改b-demo的build文件,再按同样的方式打包:

最后,在a-demo中引入b-demo,并刷新Gradle引用。

依赖树打印

这里有两种方式可以查看依赖关系,一种是gradle自带的依赖树打印功能gradlew dependencies,另一种则是使用IDEA中的 Dependency Analyzer工具。

我们先看看第一种方式,要看a-demo的依赖关系,首先打开终端控制台,进入a-demo模块,然后再利用根目录的gradlew文件执行打印依赖树指令:

shell 复制代码
cd a-demo
../gradlew dependencies

在上图中,我们可以看到编译时路径运行时路径 的依赖关系。

  • 编译时路径中:
    b-demolombok,其中 b-demo 的依赖配置是 implementation,上面提到了,这种依赖配置会将jar包添加到编译时和运行时 两种依赖路径中,lombok 是在根项目中配置的compileOnly,每个子模块都继承了这个配置,所以可以看到在编译时路径中会存在,也正是因为在编译时存在,在代码中才可以直接使用。
    可以看到的是,目前的配置中,编译时并没有发生依赖传递(即没有出现间接依赖)。
  • 运行时路径中:
    我们可以看到的是,在运行时路径中除了b-demo这个直接依赖以外,还有c-demo这个间接依赖,这是因为 implementation 可以实现包在运行时路径上传递。
使用 Dependency Analyzer 查看依赖

如果觉得上面这种打印依赖树的方式有点繁琐,还可以使用IDEA的自带工具(好像是2022以上版本就有了)。如下图,可以直观的看到编译时和运行时 的依赖关系,和上面打印出的结果是一致的。

如果想看到更详细的信息,可以选择红框中的任意一个包,右键选择Analyze Dependencies进入到工具弹窗中。

通过这里的一系列操作就可以查看到自己想看的依赖关系了,举个例子:假如我现在想看a-demo的运行时依赖关系,通过树状展示,可以这么做。

2.4.3.编译时依赖传递

编译时依赖传递需要通过api这个依赖配置实现,通过java-library插件引入。在根项目的subprojects中已经设置了apply plugin: 'java-library',这里可以直接使用。

现在我希望将b-demo中引入的c-demo包,在编译时和运行时 都通过,通过间接依赖的形式传递给a-demo,只需要修改B模块中的build文件,将 implementation 修改为 api,修改完成后重新打包。在a-demo里面引入b。

  • b-demo:

    groovy 复制代码
    version = '1.0.1'
    
    dependencies {
        api 'com.ls:c-demo:1.0.0'
    }
  • a-demo:

    groovy 复制代码
    version = '1.0.0'
    
    dependencies {
        implementation 'com.ls:b-demo:1.0.1'
    }

最后的结果是,两种路径的依赖都传递了:

2.5.依赖冲突

简单的说,依赖冲突就是在一个项目中引入了 groupId 和 ArtifactId 一样,但 version 不一样的jar包,程序在使用时不能确定要使用哪一个包。


《依赖解析官网文档》

2.5.1.依赖解析(dependency resolution)

不管是Maven还是Gradle都有处理依赖冲突的"调解机制",在Gradle中叫"依赖解析(dependency resolution)"机制,两者之间还是有一定的区别,在工作中需要注意,不能混为一谈。

  • Maven的处理方式是通过"就近原则",即选择依赖路径中最近的那个版本,详情可以看这篇文章《Maven坐标的概念与规则》
  • Gradle使用的是"最新版本优先"策略,在有多个版本冲突时,Gradle 默认选择最高的版本。

在判断多个版本的高低时,是通过定义好的版本排序机制来处理的,如果想对这个排序机制有比较详细的了解,可以去看看《版本排序官方文档》,下面先聊一下版本号的组成规则,再解释一下这个排序机制。

版本号的组成

版本号实际上是由两部分组成的:BaseVersion+Qualifier

  • BaseVersion:基础版本号,规范的情况下由 x.y.z 3个数字组成,但也有其他字母组成的情况。
  • Qualifier:限定符,规范的情况下有beta,SNAPSHOT,RELEASE,GA等,也可能是其他不规则的符号,或者没有限定符。

下面的官网提供的例子,如何分解基础版本号与限定符:

在版本号里面有.,+,-,_这样的分隔符,以及字母和数字之间有一个空串分隔符,出现这些分隔符的时候会找第一个不是.的分隔符,左侧就是基础版本号,右侧就是限定符。

版本号排序规则
  • 第一步:通过分隔符将版本号拆分成不同的组成部分
    通过.,+,-,_这几个符号进行拆分,同时,如果有字母和数字的组合 ,也会拆解成不同的部分。
    例如:1a11.a.11-a-1 不管分隔符是什么,都会拆分成[1,a,1]三个部分。
  • 第二步:分别对比每个部分的值
    就像字符串的对比那样,从左到右,一个一个的对比,其大小规则为:2 > 1 > 0 > b > a > B > Am遇到多个部分组合比较的时候,还要注意一个规则:1.1.0 > 1.1 > 1.1.a
  • 第三步:特殊含义的限定符比较
    比较规范的版本号中,右侧的限定符一般是用来标识当前的jar包状态的,例如是开发状态,还是发布状态,是测试版,还是稳定版等等。
    这种情况也有优先级顺序:1.0-dev < 1.0-alpha < 1.0-zeta < 1.0-rc < 1.0-snapshot < 1.0-final < 1.0-ga < 1.0-release < 1.0-sp < 1.0

上面的规则比较多,不太方便记忆,但是也没关系,在大多数情况下版本号都是比较规范的,只需要比较数字的大小就可以了。


2.5.2.排除传递依赖

我们在引入多种三方jar包的时候,通过依赖传递的特性,可能会间接引入了某一个jar包的多个版本,但我们并不想使用Gradle自动指定的最高版本,这时候我们就可以通过排除传递依赖的方式,把最高版本的包排除掉。《排除传递依赖官方文档》

单个排除

语法也比较简单,在implementation引入依赖的时候,通过exclue排除依赖就可以了(需要将语法由空格隔开,调整为用小括号包裹)。exclue的参数常用的有4种组合,分别对应不同的功能:

  • 排除所有传递依赖
    artifactIdexclue的参数中使用module标记。

    groovy 复制代码
    implementation("com.ls:demo-api:1.0.0") {
        exclude(group: '*')
    }
    
    implementation("com.ls:demo-api:1.0.0") {
        exclude(module: '*')
    }
  • 指定排除某个groupId的依赖

    groovy 复制代码
    implementation("com.ls:demo-api:1.0.0") {
         exclude(group: 'org.slf4j')
    }
  • 指定排除某个构件

    groovy 复制代码
    implementation("com.ls:demo-api:1.0.0") {
         exclude(module: 'log4j')
    }
  • 指定排除某个groupId下的某个构件依赖

    groovy 复制代码
    implementation("com.ls:demo-api:1.0.0") {
        exclude group: 'com.ls', module: 'c-demo'
    }

全局排除

除了一个一个的排除以外,有时候我们还想一次排除项目中的所有指定的传递依赖,例如前两年的log4j包出现了安全问题,需要把它从全局排除掉。

以上面多个子模块的demo项目为例,先查依赖找到log4j包:

groovy 复制代码
// 根项目的 build.gradle 文件
subprojects {
    configurations.configureEach {
        exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
        exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
    }
}

说明:configurations.configureEach 表示在所有的依赖配置 中都排除掉,是Gradle6以后得功能,旧版本里面可以使用configurations.all,排除后的结果如下图。


如果只想排除一定的依赖配置,可以通过下面的语法来实现:

groovy 复制代码
// 根项目的 build.gradle 文件
subprojects {
    configurations {
        compileOnly.exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
        implementation.exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
    }
}

2.5.3.强制指定版本号

可以通过force来强制使用某个版本号,这个功能也是在configurations 中进行配置,平时使用的不多,这里就简单提一下:

groovy 复制代码
configurations.all {
    resolutionStrategy {
        force 'com.ls:c-demo:1.0.1'
    }
}

指定了之后,就不会管依赖传递中c-demo的版本号了,通通使用1.0.1版本。

3.总结

本文着重针对 Gradle 的依赖管理展开了全方位且极为详尽的介绍。在此过程中,清晰明确地提及了诸如坐标、依赖的基础概念、依赖配置、依赖传递还有依赖冲突等相关特性。在充分熟知并掌握了这些特性之后,于后续开展的工作当中,就能够更加精确、细致地对依赖进行管理,并且可以妥善、有效地处理由依赖冲突所引发的各种各样的问题。

相关推荐
代码之光_198024 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi29 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
对许1 小时前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
无尽的大道1 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力1 小时前
Java类和对象(下篇)
java
binishuaio1 小时前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE1 小时前
【Java SE】StringBuffer
java·开发语言
老友@1 小时前
aspose如何获取PPT放映页“切换”的“持续时间”值
java·powerpoint·aspose
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式