依赖Contents

  • 什么是依赖配置
  • 可解析的可消费的配置
  • 为依赖选择正确的配置
  • 定义自定义的配置
  • 不同种类的依赖
  • 记录依赖
  • 从模块依赖中解析指定的工件
  • 支持的元数据格式

在查看依赖声明之前,依赖配置的概念需要被定义 …

什么是依赖配置

在Gradle 项目中 每一个依赖声明都需要指定一个scope ..
举个例子,某些依赖应该被用来编译源代码,因为其他的仅仅在运行时被需要 …
Gradle 借助Configuration 的帮助来呈现一个依赖的范围 。。
每一个配置能够通过独一无二的名称进行识别 ..

许多Gradle 插件增加了预定义的configuration 到项目中,例如Java 插件 ..
增加了configuration 去呈现各种不同的类路径(为了源代码编译需要,执行测试以及其他) …
The Java Plugin

让我们看一下为了特定目的而声明的依赖配置关系图
声明依赖 - 图1
如上图所示,直接测试和编译源码文件所用到的配置不一样,并且工作方式也有所不同 ..
用于导航、检查和后处理已分配依赖项的元数据和工件的配置使用,查看resolution result APIs .

配置继承和组合

一个配置能够继承其他配置 - 也就是具有继承体系 ..
子配置继承父配置的任何声明依赖完整集合 ..
配置继承在Gradle 核心插件 - Java 插件深度使用 .. , 例如 testImplementation 配置继承于 implementation 配置 .
配置体系有一个实际性的目的: 编译测试需要在编写测试类的依赖之上 + 源代码的依赖 …
一个使用Junit的Java 项目去编写 并执行 测试代码 同样需要Guava(如果这些类在源代码中也有导入) …
那么,通过如下配置关系 就能够阐述配置继承的必要性了:
声明依赖 - 图2
所以 测试本身需要编写测试代码本身的依赖 + 被测试代码的依赖 … 通过依赖继承能够完美解决. ..

对于testImplementation 和 implementation 配置的继承体系 能够通过调用Configuration#extendsFrom( … Configurations) 进行构建 ..
一个配置能够继承任何其他的配置 - 而无关它在构建脚本或者插件中的定义 …

冒烟测试示例配置

假设您想编写一套冒烟测试. 每一个冒烟测试使用Http 调用验证一个 web 服务端点 ..
由于项目的测试框架已经使用了JUnit .. 你能够定义一个名叫 smokeTest的新配置 - 让它继承于 testImplementation 配置去重用存在的测试框架依赖 …
配置如下:

  1. configurations {
  2. smokeTest.extendsFrom testImplementation
  3. }
  4. dependencies {
  5. testImplementation 'junit:junit:4.13'
  6. smokeTest 'org.apache.httpcomponents:httpclient:4.5.5'
  7. }

可解析和可消费的配置

在Gradle 中 配置作为依赖解析的基础部分 .. 在依赖解析的上下文中,它是被用来区分谁是消费者,谁是生产者 .. 沿着这个思路 , 配置至少有三个不同的角色:

  • 声明依赖
  • 作为消费者,为了解析依赖集合为文件
  • 作为生产者,暴露工件以及它的依赖 由其他项目进行消费 …(例如 可消费的配置通常表现为生产者提供给它的消费者的一个变种) ..

举个例子,为了表达app 应用依赖于lib库, 至少一个配置是需要的:

configurations {
    // 声明一个配置,叫做 someconfiguration
    somConfiguration
}


dependencies {
// 增加一个项目依赖到 "someConfiguration" 配置上
     someConfiguration project(":lib")   
}

配置能够继承它所继承的configurations 的依赖, 现在注意上述的代码不会告诉我们关于这个配置的预期消费者的任何信息 …
尤其是,他没有告诉我们这个配置打算怎么使用 … 假设 lib 是一个Java 库,它可能暴露了不同的事情,例如它的API,implementation / 测试装置 ..
它可能有必要去改变我们如何解析app 的依赖 - 取决于我们执行的任务(针对 lib的API 编译,执行应用,编译测试,等等) ….
为了解释这个问题,你将经常发现伴生配置 , 这些打算明确声明这个使用:

Configurations representing concrete dependency graphs

configurations {
    // 声明一个配置  它用来解析这个应用的编译类路径
    compileClasspath.extendsFrom(someConfiguration)

    // 声明一个配置  它用来解析这个应用的运行时类路径
    runtimeClasspath.extendsFrom(someConfiguration)
}

此时,我们具有3个不同角色的不同配置:

  • someConfiguration 声明了我们应用的依赖,它仅仅是一个(bucket - 桶) - 持有依赖列表
  • compileClasspath 以及 runtimeClasspath 配置打算被解析: 当解析时它们应该各自包含编译类路径,以及应用的运行时类路径 …

这个区分通过Configuration类型的 canBeResolved 属性进行标识 ..
一个配置能够被解析的条件是: 一个配置 - 对此我们能够计算依赖图,因为它包含了所有必要的信息(为了解析发生所需要的) ..
那就是说,我们将要计算依赖图,在图中解析组件、甚至是获取工件 …
一个配置如果canBeResolved 设置为 false 标志着它不能被解析 … 例如一个配置仅仅时为了声明依赖 …
原因依赖于使用(编译类路径,运行时类路径) … 它能够解析为不同的图 ..
如果尝试对无法解析的配置进行解析 将会得到一个错误 ..
在一定程度上,这类似于它没有打算实例化的抽象类(canBeResolved = false) 或者继承于抽象类的具体类(canBeResolved = true), 一个可解决的配置至少有一个不可解析的配置(也许可能有多个) …
另一个方面,在库项目这边(生产者),我们也是用配置呈现那些能够被消费 …
例如,库可能会公开一个 API 或运行时,我们会将工件附加到其中一个、另一个或两者。
通常来说,为了编译lib,我们需要lib的API,但是我们不需要它的运行时依赖,因此 lib 项目将会暴露一个apiElements 配置, 它指示了消费者可以看到它的API …
例如一个配置可消费的,但是不意味着是可解析的,表达出来就是Configuration中的canBeConsumed flag …

configurations {
    // 一个打算让消费者能够看到此组件的API 的配置
    exposedApi {
        // 这个配置是一个外出配置,并不打算能够解析
        canBeResolved = false
        // 输出配置,解释消费者能够消费它
        canBeConsumed = true
    }

    // 打算为消费者暴露这个组件的实现的配置
    exposedRuntime {
        canBeResolved = false
        canBeConsumed = true
    }
}

还击话说,一个配置的角色 由canBeResolved 以及 canBeConsumed flag 合并决定 …

配置角色

Configuration role can be resolved can be consumed
Bucket of dependencies 依赖的桶(仅仅用来保存依赖信息) false false
Resolve for certain usage(为某些使用进行解析) true false
Exposed to consumers(暴露给消费者) … false true
Legacy, don’t use true true

为了向后兼容,两个标志默认值都是true, 但是作为一个插件开发者,应该总是决定这些flag的正确值,否则可能会引入意外的解析错误 ….

为依赖选择正确的配置

配置的选择在声明依赖的时候非常重要,然而这里没有固定的法则能够套用 依赖的配置的选择 ..
这主要取决于配置的组织方式,它们大多数应用插件的一个属性 …(一般的插件都提供的对应的Configuration) …
举个例子,Java 插件 创建了一些配置 并且它们作为声明依赖的基础(为你的代码,基于它的角色声明依赖)
所以我们应该进行基于角色进行选择 声明依赖 …
值得一提的是,插件应该清楚地记录它们的配置链接在一起的方式,并且应该尽可能地隔离它们的角色。

定义自定义配置

你能够自己定义配置,因此叫做自定义configuration .. 一个自定义的configuration 通常是有用的(为了分离依赖的范围用于专用的目的)
假设你想要在Jasper Ant task 上声明一个依赖 - 为了预编译JSP 文件,那么这个依赖不应该放置在编译类路径上 …
它非常简单的实现此目的 - 只需要通过引入一个自定义的配置并且在任务中使用它 ..

configurations {
    jasper 
}

repositories {
    mavenCentral() 
}

dependencies {
    jasper 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.2'
}

tasks.register('preCompileJsps') {
    doLast {
        ant.taskdef(classname: 'org.apache.jasper.JspC',
                    name: 'jasper',
                    classpath: configurations.jasper.asPath)

        ant.jasper(validateXml: false,
                   uriroot: file('src/main/webapp'),
                   outputDir: file("$buildDir/compiled-jsps"))
    }
}

一个项目的配置 将通过configurations 对象进行管理, 每一个configuration 都有一个名称 并且能够继承于其他的configuration … 点此了解它的API …

不同种类的依赖

模块依赖

模块依赖是大多数通用依赖, 它们指向一个仓库中的模块 ..

dependencies {
    runtimeOnly group: 'org.springframework', name: 'spring-core', version: '2.5'
    runtimeOnly 'org.springframework:spring-core:2.5',
            'org.springframework:spring-aop:2.5'
    runtimeOnly(
        [group: 'org.springframework', name: 'spring-core', version: '2.5'],
        [group: 'org.springframework', name: 'spring-aop', version: '2.5']
    )
    runtimeOnly('org.hibernate:hibernate:3.0.5') {
        transitive = true
    }
    runtimeOnly group: 'org.hibernate', name: 'hibernate', version: '3.0.5', transitive: true
    runtimeOnly(group: 'org.hibernate', name: 'hibernate', version: '3.0.5') {
        transitive = true
    }
}

查看DependencyHandler 了解更多示例和完整的参考 ..
Gradle 提供了模块依赖的不同的标记 .. 例如字符串标记 / map 标记 …
一个模块依赖允许更深度的配置 - 通过API ,查看ExternalModuleDependency去了解这个API ..
此API 提供了属性和配置方法 .. 为了访问完整的API ,那么使用map 或者使用 字符串标记 ..
你能够定义单个依赖并结合闭包进行深度配置 …

如果你声明了一个模块依赖,Gradle 会在仓库中查找模块元数据文件(.module,.pom或者 ivy.xml) … 如果存在这样的模块元数据文件,它将被解析并且模块的工件(例如 hibernate-3.0.5.jar)同样它的依赖(例如 cglib)会被下载 …,如果没有这样的元数据文件存在,在Gradle 6.0,你需要配置metadata sources definitions 去直接查找一个名叫 hibernate-3.0.5.jar ..

在Maven 中,一个模块有且只有一个工件

在Gradle 和 lvy中,一个模块能够有多个工件,每一个工件有不同的依赖集合 ..

文件依赖

项目有些时候不仅仅依赖了一些二进制仓库产品 . 例如 JFrog Artifactory or Sonatype Nexus 用来托管并解析外部依赖 …
经验通常告诉我们这些依赖最好放置在一个共享驱动器中或者将它们与项目源代码一起到版本控制中检查 …
这些依赖称为 文件依赖,这个原因是它们是文件而没有元数据(例如传递性依赖,来源/ 作者的信息) 依附于它们…
例如,从本地文件系统以及共享驱动盘上解析文件依赖
声明依赖 - 图3
以下例子展示了如何从ant / libs / tools目录中解析文件依赖 ..

configurations {
    antContrib
    externalLibs
    deploymentTools
}

dependencies {
    antContrib files('ant/antcontrib.jar')
    externalLibs files('libs/commons-lang.jar','libs/log4j.jar')
    deploymentTools(fileTree('tools') {include '*.exe'})
}

正如你所见,每一个依赖都定义了在文件系统上的准确位置 .. 创建文件引用最好的方法是
Project.files … ProjectLayout.files … Project.fileTree … 这些内容在Gradle 和 文件协同工作的章节中 着重讲解了 …
除此之外,你也能够定义 一个来自于 flat directory repository形式的一个或者多个文件依赖的源目录 ..

在FileTree中的文件的顺序不是固定的,甚至在单个计算机上 .. 这意味着以这种结构为种子的依赖配置可能会产生具有不同排序的解析结果, 可能会影响任务的缓存能力(将这个结果作为输入的任务) ,使用更加简单的files 在这种情况下是更推荐的 ..

文件依赖允许你直接增加一批文件到配置中 .. 而不需要先将他们增加到仓库中 ..
如果你不想要或者不同增加到仓库中,这是非常有用的 .. 或者从始至终你都不想要使用任何仓库来存储你的依赖 …
为了增加文件作为依赖,你可以简单的传递一个 file collection 作为依赖 ..

dependencies {
    runtimeOnly files('libs/a.jar', 'libs/b.jar')
    runtimeOnly fileTree('libs') { include '*.jar' }
}

文件依赖并不会包括在你项目的已发布依赖的描述符中 .. 然而文件依赖会包括在相同构建中的传递性项目依赖项中 …
这意味你不能够在当前构建之外进行使用 ,仅仅使用在相同构建中 …
FileTree的副作用 并不保证集合中的文件总是顺序一致的,所以在具有缓存能力且将这个结果作为输入的任务中,最好使用files …
你能够生成一些任务用来为一个文件依赖产生 文件 … 当你想要这样做的时候,尝试一下这个,构建将生成这些文件

dependencies {
    implementation files(layout.buildDirectory.dir('classes')) {
        // 执行任务
        builtBy 'compile'
    }
}

tasks.register('compile') {
    doLast {
        println 'compiling classes'
    }
}

tasks.register('list') {
  dependsOn configurations.compileClasspath
    doLast {
        println "classpath = ${configurations.compileClasspath.collect { File file -> file.name }}"
    }
}
$ gradle -q list
compiling classes
classpath = [classes]

文件依赖的版本

表达文件依赖的意图和具体版本是非常推荐的,文件依赖不会被Gradle 的 版本冲突解决 考虑 ..
因此,为文件名分配一个版本以指示其附带的不同更改集是非常重要的 ..
例如 commons-beanutils-1.3.jar ,你能够通过发行注意通知跟踪这个库的改变 …
因此,这个项目的依赖更容易维护和组织 .. 它更容易去规避潜在的API 不兼容性 - 通过指定版本号 ..

项目依赖

软件项目经常分离出软件组件到 模块中提高维护性并阻止强耦合 ..
模块也能够定义依赖,在相同项目中模块之间能够重用代码 …
例如多项目构建:
声明依赖 - 图4
Gradle 能够根据依赖关系在模块之间进行建模 … 这些依赖叫做项目依赖 ,因为每一个模块都是Gradle 项目 …

dependencies {
    implementation project(':shared')
}

在运行时,这个构建会自动的确保这些内置的项目依赖是通过正确的顺序 将他们增加到类路径中进行编译 …
组建多项目构建 更加详细的讨论了如何构建并配置多项目构建 ..
也可以查看项目依赖的API 文档了解更多信息 …

例如web-service 项目依赖于utils / api 项目 ,那么 Project.project(java.lang.String)) 能够被用来创建一个依赖(通过指定路径 引用一个特定的项目) ….

dependencies {
    implementation project(':utils')
    implementation project(':api')
}

类型安全的项目依赖

类型安全的项目访问器是一个孵化的特性 - 它必须显式的启用 .. 这个实现也许在任意时刻改变,为了增加对类型安全的项目访问器支持,可以在settings.gradle(.kts)文件中增加: enableFeaturePreview(“TYPESAFE_PROJECT_ACCESSORS”)

project(“:some:path”) 表示法的一个问题是您必须记住要依赖的每个项目的路径.
除此之外,当你改变项目路径的时候你需要改变所有使用了此项目依赖的路径 …
这样可能会出现一个或者多个错误 (因为你依赖于 查询和替换) …
自从Gradle 7开始,Gradle 为项目依赖提供了一个实验性的类型安全的API,例如:

dependencies {
    implementation projects.utils
    implementation projects.api
}

类型安全 API 具有提供 IDE 补全的优势,因此您无需弄清楚项目的实际名称。
如果你(增加一个项目或者删除一个项目)并且使用Kotlin DSL,构建脚本的编译可能会失败(这种情况下你可能是忘记更新依赖) …
项目访问器是根据项目路径进行映射的,举个例子,如果你的项目路径是 :commons:utils:some:lib,那么项目访问器就是 projects.commons.utils.some.lib(这是projects.getCommons().getUtils().getSome().getLib()的简短表示法) …

项目名称可以是烤肉串形式 ‘或者蛇形式 它们都能够转换为驼峰形式(在访问器形式下) …
于是 som-lib / some_lib 在访问器模式下 表示为 projects.someLib …

模块依赖的本地分支

一个模块依赖能够通过一个依赖替换为 模块的资源的本地分支 … 并且如果这个分支项目是通过Gradle 构建的, 什么意思呢,它将使用组合构建 ..
它允许你,例如去修复一个在application使用的一个库中的问题,并构建,发布一个本地修补版本 替代发布的二进制版本 ..
详情了解组合构建 …

Gradle 特定发行依赖

Gradle API 依赖

你能够使用Gradle 的当前版本的API (DependencyHandler.gradleApi()))去声明依赖 ..
这是有用的 - 当你开发自定义Gradle 任务和插件的时候 …

dependencies {
    implementation gradleApi()
}

Gradle TestKit 依赖

你能够使用当前版本的Gradle 的API(DependencyHandler.gradleTestKit())) 声明依赖 ..
为Gradle 插件和构建脚本编写并执行功能性测试是非常有用的 …

dependencies {
    testImplementation gradleTestKit()
}

The TestKit chapter 描述了使用TestKit的细节 ..

本地Groovy 依赖

您可以使用 DependencyHandler.localGroovy()) 方法声明对与 Gradle 一起分发的 Groovy 的依赖项。当您在 Groovy 中开发自定义 Gradle 任务或插件时,这很有用。

dependencies {
    implementation localGroovy()
}

依赖注释

声明依赖项或依赖项约束时,可以为声明提供自定义原因。这使得构建脚本中的依赖声明和依赖洞察报告更容易解释。

plugins {
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation('org.ow2.asm:asm:7.1') {
        because 'we require a JDK 9 compatible bytecode generator'
    }
}

使用自定义原因进行依赖省查报告

> gradle -q dependencyInsight --dependency asm
org.ow2.asm:asm:7.1
variant "compile" [
org.gradle.status              = release (not requested)
org.gradle.usage               = java-api
org.gradle.libraryelements     = jar (compatible with: classes)
org.gradle.category            = library

Requested attributes not found in the selected variant:
org.gradle.dependency.bundling = external
org.gradle.jvm.environment     = standard-jvm
org.gradle.jvm.version = 11
]
Selection reasons:
- Was requested : we require a JDK 9 compatible bytecode generator

org.ow2.asm:asm:7.1
\--- compileClasspath

A web-based, searchable dependency report is available by adding the --scan option.

解析来自模块依赖的指定工件

我们怎样,Gradle 将尝试从Maven 或者 Lvy仓库解析一个模块 .. 它查询元数据文件以及默认的工件文件,一个Jar ..
如果没有这样的工件文件能够被解析那么构建将失败 … 在某些条件下,您可能想要调整 Gradle 为依赖项解析工件的方式。

  • 这个依赖仅仅提供了一个不标准的工件没有任何元数据(例如 ZIP 文件) …
  • 模块元数据声明一个或者多个工件(例如lvy依赖描述符的一部分) ..
  • 你仅仅想下载指定的工件且在它们的元数据中没有声明任何传递性依赖 ..

Gradle 是一个多语言构建工具 - 并没有限制你只能解析Java 库 ..
假设你想要使用JavaScript作为客户端技术构建一个web 应用 … 大多数项目会将外部js库放入版本控制 ..
一个外部js库 和 重用 java 库没有什么不同(因此为什么不从仓库下载进行替代呢?)
Google 托管的库 是一个受欢迎的且开源的js 库发行平台 ..
通过仅工件的标识符的帮助下你能够下载一个js 库,例如 JQuery, @ 字符将依赖项的坐标与工件的文件扩展名分开。

repositories {
    ivy {
        url 'https://ajax.googleapis.com/ajax/libs'
        patternLayout {
            artifact '[organization]/[revision]/[module].[ext]'
        }
        metadataSources {
            artifact()
        }
    }
}

configurations {
    js
}

dependencies {
    js 'jquery:jquery:3.2.1@js'
}

某些模块关联了相同工件的不同喜好 或者它们发布的工件都属于一个特定的模块版本但是有不同的目的 …
Java 库发布带有编译后的类文件的工件是很常见的,另一个发布只有源代码,第三个发布包含 Javadocs。
在js中,一个库文件也许是未压缩或者最小化的工件 .. 在Gradle中一个特定的工件标识符叫做classifier(分类符),这是一个在Maven 或者 Lvy依赖管理中通常使用的术语 ..
假设我们想要下载一个Jquery 库的最小工件 而不是未压缩文件 .. 你能够提供一个min 分类符 作为依赖声明的一部分 …

通过依赖标识符解析js 工件来声明依赖

repositories {
    ivy {
        url 'https://ajax.googleapis.com/ajax/libs'
        patternLayout {
            artifact '[organization]/[revision]/[module](.[classifier]).[ext]'
        }
        metadataSources {
            artifact()
        }
    }
}

configurations {
    js
}

dependencies {
    js 'jquery:jquery:3.2.1:min@js'
}

支持的元数据格式

外部模块依赖需要模块元数据(例如 Gradle 能够发现一个模块的传递性依赖) …
因此Gradle 支持不同的元数据格式 ..
你能够调整格式(通过查看仓库定义) …

Gradle 模块元数据 文件

Gradle 模块元数据已经被设定设计为支持Gradle 依赖管理模型的所有特性并且它是偏爱的格式 … 点此查看规范

POM files

Gradle 原生支持 Maven Pom files .. 值得注意的是Gradle 首先会查看POM 文件 .. 但是如果这个文件包含了特定的标记,Gradle 将使用 Gradle Module Metadata 进行替代 ..

Lvy files

类似的,Gradle 同样支持Apache lvy metadata files .., 同样,Gradle 首先查找ivy.xml 文件,但是如果这个文件包含了一个特定的marker,Gradle 将使用 Gradle 模块元数据进行替代 …