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

相关推荐
cainiao0806051 小时前
Java 大视界:基于 Java 的大数据可视化在智慧城市能源消耗动态监测与优化决策中的应用(2025 实战全景)
java
长风破浪会有时呀2 小时前
记一次接口优化历程 CountDownLatch
java
云朵大王2 小时前
SQL 视图与事务知识点详解及练习题
java·大数据·数据库
我爱Jack3 小时前
深入解析 LinkedList
java·开发语言
一线大码3 小时前
Gradle 高级篇之构建多模块项目的方法
spring boot·gradle·intellij idea
27669582924 小时前
tiktok 弹幕 逆向分析
java·python·tiktok·tiktok弹幕·tiktok弹幕逆向分析·a-bogus·x-gnarly
用户40315986396634 小时前
多窗口事件分发系统
java·算法
用户40315986396634 小时前
ARP 缓存与报文转发模拟
java·算法
小林ixn4 小时前
大一新手小白跟黑马学习的第一个图形化项目:拼图小游戏(java)
java
nbsaas-boot4 小时前
Go语言生态成熟度分析:为何Go还无法像Java那样实现注解式框架?
java·开发语言·golang