Gradle 用户手册:版本 8.7
概述
梯度用户手册
Gradle 构建工具
Gradle Build Tool 是一种快速、可靠且适应性强的开源构建自动化工具,具有优雅且可扩展的声明性构建语言。
在本用户手册中,Gradle Build Tool 缩写为Gradle。
为什么是摇篮?
Gradle 是一种广泛使用且成熟的工具,拥有活跃的社区和强大的开发者生态系统。
-
Gradle 是最流行的 JVM 构建系统,也是 Android 和 Kotlin 多平台项目的默认系统。它拥有丰富的社区插件生态系统。
-
Gradle 可以使用其内置功能、第三方插件或自定义构建逻辑来自动化各种软件构建场景。
-
Gradle 提供了一种高级的、声明性的、富有表现力的构建语言,使构建逻辑的阅读和编写变得容易。
-
Gradle 速度快、可扩展,并且可以构建任何规模和复杂性的项目。
-
Gradle 生成可靠的结果,同时受益于增量构建、构建缓存和并行执行等优化。
Gradle, Inc. 提供名为Build Scan®的免费服务,可提供有关构建的广泛信息和见解。您可以查看扫描以识别问题或共享它们以获取调试帮助。
兼容的 IDE
所有主要 IDE 都支持 Gradle,包括 Android Studio、IntelliJ IDEA、Visual Studio Code、Eclipse 和 NetBeans。
您还可以通过终端中的命令行界面(CLI) 或通过持续集成 (CI) 服务器调用 Gradle。
教育
Gradle用户手册是 Gradle 构建工具的官方文档。
-
入门教程—了解 Gradle 基础知识以及使用 Gradle 构建应用程序的好处。
-
培训课程— 前往课程页面注册免费 Gradle 培训。
许可证
Gradle Build Tool 源代码是开放的,并根据Apache License 2.0获得许可。 Gradle 用户手册和 DSL 参考手册已根据Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License获得许可。
用户手册
浏览我们的 Gradle 使用指南和示例。
内容
Gradle 用户手册分为以下部分:
- 运行 Gradle 构建
-
了解 Gradle 基础知识以及如何使用 Gradle 构建项目。
- 编写 Gradle 构建
-
开发任务和插件来定制您的构建。
- 编写 JVM 构建
-
将 Gradle 与您的 Java 项目结合使用。
- 使用依赖项
-
将依赖项添加到您的构建中。
- 优化构建
-
使用缓存来优化您的构建并了解 Gradle 守护进程、增量构建和文件系统监视。
- CI 上的 Gradle
-
Gradle 与流行的持续集成 (CI) 服务器集成。
发布
安装 Gradle
摇篮安装
如果您只想运行现有的 Gradle 项目,并且构建使用Gradle Wrapper ,则无需安装 Gradle 。这可以通过项目根目录中是否存在gradlew
或文件来识别:gradlew.bat
. // (1) ├── gradle │ └── wrapper // (2) ├── gradlew // (3) ├── gradlew.bat // (3) └── ⋮
-
项目根目录。
-
用于执行 Gradle 构建的脚本。
如果gradlew
或gradlew.bat
文件已存在于您的项目中,则无需安装 Gradle。但您需要确保您的系统满足 Gradle 的先决条件。
如果您想更新项目的 Gradle 版本,可以按照升级 Gradle 部分中的步骤进行操作。请使用Gradle Wrapper升级 Gradle。
Android Studio 附带了 Gradle 的有效安装,因此当您仅在该 IDE 中工作时,无需单独安装 Gradle。
如果您不满足上述条件并决定在计算机上安装 Gradle,请首先通过gradle -v
在终端中运行来检查 Gradle 是否已安装。如果该命令没有返回任何内容,则 Gradle 尚未安装,您可以按照以下说明进行操作。
您可以在 Linux、macOS 或 Windows 上安装 Gradle Build Tool。安装可以手动完成,也可以使用像SDKMAN 这样的包管理器完成!或自制。
您可以在版本页面上找到所有 Gradle 版本及其校验和。
先决条件
Gradle 可以在所有主要操作系统上运行。它需要Java 开发工具包(JDK) 版本 8 或更高版本才能运行。您可以检查兼容性矩阵以获取更多信息。
要检查,请运行java -version
:
❯ java -version openjdk version "11.0.18" 2023-01-17 OpenJDK Runtime Environment Homebrew (build 11.0.18+0) OpenJDK 64-Bit Server VM Homebrew (build 11.0.18+0, mixed mode)
❯ java version "1.8.0_151" Java(TM) SE Runtime Environment (build 1.8.0_151-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
Gradle 使用它在您的路径中找到的 JDK、IDE 使用的 JDK 或项目指定的 JDK。在此示例中,$PATH 指向 JDK17:
❯ echo $PATH /opt/homebrew/opt/openjdk@17/bin
您还可以将JAVA_HOME
环境变量设置为指向特定的JDK安装目录。当安装了多个 JDK 时,这尤其有用:
❯ echo %JAVA_HOME% C:\Program Files\Java\jdk1.7.0_80
❯ echo $JAVA_HOME /Library/Java/JavaVirtualMachines/jdk-16.jdk/Contents/Home
Linux安装
使用包管理器安装
SDKMAN!是一个用于管理大多数类 Unix 系统(macOS、Linux、Cygwin、Solaris 和 FreeBSD)上多个软件开发套件的并行版本的工具。 Gradle由SDKMAN部署和维护!:
❯ sdk install gradle
其他包管理器也可用,但它们分发的 Gradle 版本不受 Gradle, Inc. 控制。Linux 包管理器可能会分发与官方版本不兼容或不完整的 Gradle 修改版本。
手动安装
第 1 步 -下载最新的 Gradle 发行版
分发 ZIP 文件有两种形式:
-
仅二进制 (bin)
-
完整(全部)包含文档和来源
我们建议下载bin文件;它是一个较小的文件,可以快速下载(并且可以在线获取最新的文档)。
第 2 步 - 解压发行版
将分发 zip 文件解压缩到您选择的目录中,例如:
❯ mkdir /opt/gradle ❯ unzip -d /opt/gradle gradle-8.7-bin.zip ❯ ls /opt/gradle/gradle-8.7 LICENSE NOTICE bin README init.d lib media
第 3 步 - 配置您的系统环境
要安装 Gradle,解压文件的路径需要位于您的 Path 中。配置PATH
环境变量以包含bin
解压发行版的目录,例如:
❯ export PATH=$PATH:/opt/gradle/gradle-8.7/bin
或者,您也可以添加环境变量GRADLE_HOME
并将其指向解压缩的发行版。PATH
您可以将特定版本的 Gradle 添加$GRADLE_HOME/bin
到您的PATH
.升级到不同版本的 Gradle 时,只需更改GRADLE_HOME
环境变量即可。
export GRADLE_HOME=/opt/gradle/gradle-8.7 export PATH=${GRADLE_HOME}/bin:${PATH}
macOS安装
手动安装
第 1 步 -下载最新的 Gradle 发行版
分发 ZIP 文件有两种形式:
-
仅二进制 (bin)
-
完整(全部)包含文档和来源
我们建议下载bin文件;它是一个较小的文件,可以快速下载(并且可以在线获取最新的文档)。
第 2 步 - 解压发行版
将分发 zip 文件解压缩到您选择的目录中,例如:
❯ mkdir /usr/local/gradle ❯ unzip gradle-8.7-bin.zip -d /usr/local/gradle ❯ ls /usr/local/gradle/gradle-8.7 LICENSE NOTICE README bin init.d lib
第 3 步 - 配置您的系统环境
要安装 Gradle,解压文件的路径需要位于您的 Path 中。配置PATH
环境变量以包含bin
解压发行版的目录,例如:
❯ export PATH=$PATH:/usr/local/gradle/gradle-8.7/bin
或者,您也可以添加环境变量GRADLE_HOME
并将其指向解压缩的发行版。PATH
您可以将特定版本的 Gradle 添加$GRADLE_HOME/bin
到您的PATH
.升级到不同版本的 Gradle 时,只需更改GRADLE_HOME
环境变量即可。
最好.bash_profile
在主目录中编辑以添加GRADLE_HOME
变量:
export GRADLE_HOME=/usr/local/gradle/gradle-8.7 export PATH=$GRADLE_HOME/bin:$PATH
Windows安装
手动安装
第 1 步 -下载最新的 Gradle 发行版
分发 ZIP 文件有两种形式:
-
仅二进制 (bin)
-
完整(全部)包含文档和来源
我们建议下载 bin 文件。
第 2 步 - 解压发行版
C:\Gradle
使用文件资源管理器创建一个新目录。
打开第二个文件资源管理器窗口并转到下载 Gradle 发行版的目录。双击 ZIP 存档以公开内容。将内容文件夹拖到gradle-8.7
新创建的C:\Gradle
文件夹中。
C:\Gradle
或者,您可以使用您选择的归档工具将 Gradle 发行版 ZIP 解压。
第 3 步 - 配置您的系统环境
要安装 Gradle,解压文件的路径需要位于您的 Path 中。
在文件资源管理器中,右键单击This PC
(或Computer
) 图标,然后单击Properties
→ Advanced System Settings
→ Environmental Variables
。
在System Variables
选择下Path
,然后单击Edit
。添加一个条目C:\Gradle\gradle-8.7\bin
.单击OK
保存。
或者,您可以添加环境变量GRADLE_HOME
并将其指向解压缩的发行版。Path
您可以将特定版本的 Gradle 添加%GRADLE_HOME%\bin
到您的Path
.升级到不同版本的Gradle时,只需更改GRADLE_HOME
环境变量即可。
验证安装
打开控制台(或 Windows 命令提示符)并运行gradle -v
gradle 并显示版本,例如:
❯ gradle -v ------------------------------------------------------------ Gradle 8.7 ------------------------------------------------------------ Build time: 2023-03-03 16:41:37 UTC Revision: 7d6581558e226a580d91d399f7dfb9e3095c2b1d Kotlin: 1.8.10 Groovy: 3.0.13 Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021 JVM: 17.0.6 (Homebrew 17.0.6+0) OS: Mac OS X 13.2.1 aarch64
您可以通过下载 SHA-256 文件(可从发布页面获取)并遵循这些验证说明来验证 Gradle 发行版的完整性。
兼容性矩阵
以下部分描述了 Gradle 与多种集成的兼容性。此处未列出的版本可能有效,也可能无效。
Java
执行 Gradle 需要 8 到 21 之间的 Java 版本。尚不支持 Java 22 及更高版本。
Java 6 和 7 可用于编译,但不推荐用于测试。 Gradle 9.0 不支持使用 Java 6 和 7 进行测试。
任何完全支持的 Java 版本都可以用于编译或测试。但是,最新的 Java 版本可能仅支持编译或测试,而不支持运行 Gradle。支持是使用工具链实现的,并适用于支持工具链的所有任务。
请参阅下表了解特定 Gradle 版本支持的 Java 版本:
Java版本 | 对工具链的支持 | 支持运行 Gradle |
---|---|---|
8 |
不适用 |
2.0 |
9 |
不适用 |
4.3 |
10 |
不适用 |
4.7 |
11 |
不适用 |
5.0 |
12 |
不适用 |
5.4 |
13 |
不适用 |
6.0 |
14 |
不适用 |
6.3 |
15 |
6.7 |
6.7 |
16 |
7.0 |
7.0 |
17 号 |
7.3 |
7.3 |
18 |
7.5 |
7.5 |
19 |
7.6 |
7.6 |
20 |
8.1 |
8.3 |
21 |
8.4 |
8.5 |
22 |
8.7 |
不适用 |
23 |
不适用 |
不适用 |
Kotlin
Gradle 使用 Kotlin 1.6.10 到 2.0.0-Beta3 进行了测试。 Beta 和 RC 版本可能会也可能不会。
最低 Gradle 版本 | 嵌入式 Kotlin 版本 | Kotlin 语言版本 |
---|---|---|
5.0 |
1.3.10 |
1.3 |
5.1 |
1.3.11 |
1.3 |
5.2 |
1.3.20 |
1.3 |
5.3 |
1.3.21 |
1.3 |
5.5 |
1.3.31 |
1.3 |
5.6 |
1.3.41 |
1.3 |
6.0 |
1.3.50 |
1.3 |
6.1 |
1.3.61 |
1.3 |
6.3 |
1.3.70 |
1.3 |
6.4 |
1.3.71 |
1.3 |
6.5 |
1.3.72 |
1.3 |
6.8 |
1.4.20 |
1.3 |
7.0 |
1.4.31 |
1.4 |
7.2 |
1.5.21 |
1.4 |
7.3 |
1.5.31 |
1.4 |
7.5 |
1.6.21 |
1.4 |
7.6 |
1.7.10 |
1.4 |
8.0 |
1.8.10 |
1.8 |
8.2 |
1.8.20 |
1.8 |
8.3 |
1.9.0 |
1.8 |
8.4 |
1.9.10 |
1.8 |
8.5 |
1.9.20 |
1.8 |
8.7 |
1.9.22 |
1.8 |
Groovy
Gradle 使用 Groovy 1.5.8 到 4.0.0 进行了测试。
用 Groovy 编写的 Gradle 插件必须使用 Groovy 3.x 才能与 Gradle 和 Groovy DSL 构建脚本兼容。
安卓
Gradle 使用 Android Gradle 插件 7.3 到 8.2 进行了测试。 Alpha 和 Beta 版本可能有效,也可能无效。
功能生命周期
Gradle 正在不断开发。如生命周期终止支持部分所述,定期且频繁地(大约每六周)提供新版本。
持续改进与频繁交付相结合,可以让新功能尽早提供给用户。早期用户提供了宝贵的反馈,这些反馈已纳入开发过程。
定期将新功能交付给用户是 Gradle 平台的核心价值。
同时,API 和功能稳定性也受到非常重视,并被视为 Gradle 平台的核心价值。设计选择和自动化测试被设计到开发过程中,并由向后兼容性部分正式化。
Gradle功能生命周期旨在满足这些目标。它还向 Gradle 用户传达功能的状态。在此上下文中,术语“功能”通常指的是 API 或 DSL 方法或属性,但它不限于此定义。命令行参数和执行模式(例如构建守护进程)是其他功能的两个示例。
1. 内部
内部功能不是为公共使用而设计的,仅供 Gradle 本身使用。它们可以随时以任何方式更改,恕不另行通知。因此,我们建议避免使用此类功能。 内部功能未记录。如果它出现在本用户手册、DSL 参考或 API 参考中,则该功能不是内部功能。
内部功能可能会演变成公共功能。
2. 孵化
功能在孵化状态下引入,以便在公开之前将现实世界的反馈纳入功能中。它还为愿意测试未来潜在变化的用户提供了早期访问权限。
处于孵化状态的功能可能会在未来的 Gradle 版本中发生变化,直到它不再处于孵化状态。 Gradle 版本的孵化功能的更改将在该版本的发行说明中突出显示。新功能的孵化期根据功能的范围、复杂性和性质而有所不同。
指出了孵化的特征。在源代码中,所有正在孵化的方法/属性/类都带有incubating注释。这导致它们在 DSL 和 API 参考中具有特殊标记。
如果本用户手册中讨论了孵化功能,则将明确表示该功能处于孵化状态。
功能预览 API
功能预览 API 允许通过添加设置文件来激活某些孵化功能。各个预览功能将在发行说明中公布。enableFeaturePreview('FEATURE')
当孵化功能升级为公开或删除时,它们的功能预览标志将变得过时、无效,应从设置文件中删除。
3. 公开
非内部功能的默认状态是public。用户手册、DSL 参考或 API 参考中记录的任何未明确表示正在孵化或弃用的内容都被视为公开的。据说这些功能正在从孵化状态向公众推广。每个版本的发行说明表明该版本正在推广哪些先前孵化的功能。
公共功能在未经过弃用的情况下永远不会被删除或故意更改。所有公共功能均受向后兼容性政策的约束。
4. 已弃用
由于 Gradle 的自然演变,某些功能可能会被替换或变得无关紧要。这些功能最终将在被弃用后从 Gradle 中删除。已弃用的功能可能会变得过时,直到根据向后兼容性策略最终将其删除。
已弃用的功能已表明如此。在源代码中,所有不推荐使用的方法/属性/类都带有“@java.lang.Deprecated”注释,这反映在 DSL 和 API 参考中。在大多数情况下,已弃用的元素有替代品,这将在文档中进行描述。使用已弃用的功能将导致 Gradle 输出中出现运行时警告。
应避免使用已弃用的功能。每个版本的发行说明都会指出该版本弃用的任何功能。
向后兼容政策
Gradle 提供跨主要版本(例如, 、 等)的向后1.x
兼容性2.x
。一旦在 Gradle 版本中引入公共功能,它将无限期保留,除非被弃用。一旦被弃用,它可能会在下一个主要版本中被删除。主要版本可能会支持已弃用的功能,但这并不能得到保证。
发布报废政策
每天都会创建一个新的 Gradle 夜间构建。
这包含当天通过 Gradle 广泛的持续集成测试所做的所有更改。每晚构建可能包含可能稳定也可能不稳定的新更改。
Gradle 团队为每个次要或主要版本创建一个称为候选版本 (RC) 的预发布发行版。当短时间(通常是一周)后没有发现问题时,候选版本将升级为通用版本(GA)。如果在候选版本中发现回归,则会创建新的 RC 发行版,并重复该过程。只要发布窗口打开,候选版本就会受到支持,但它们并不打算用于生产。在 RC 阶段,错误报告非常受欢迎。
由于关键错误修复或回归,Gradle 团队可能会创建额外的补丁版本来替换最终版本。例如,Gradle 5.2.1 取代了 Gradle 5.2 版本。
一旦候选版本确定,所有功能开发都会转移到最新主要版本的下一个版本。因此,每个次要 Gradle 版本都会导致同一主要版本中的先前次要版本终止生命 (EOL)。 EOL 版本不会收到错误修复或功能向后移植。
对于主要版本,Gradle 会将关键修复程序和安全修复程序向后移植到先前主要版本中的最后一个次要版本。例如,当 Gradle 7 是最新的主要版本时,6.x 系列中发布了多个版本,包括 Gradle 6.9(及后续版本)。
因此,每个主要的 Gradle 版本都会导致:
-
以前的主要版本仅成为维护版本。它只会收到严重的错误修复和安全修复。
-
上一个版本之前的主要版本将停止使用 (EOL),并且该发行版将不会收到任何新的修复。
核心理念
摇篮基础知识
Gradle根据构建脚本中的信息自动构建、测试和部署软件。
Gradle 核心概念
插件
插件用于扩展 Gradle 的功能,并可选择向项目贡献任务。
Gradle 项目结构
许多开发人员会通过现有项目首次与 Gradle 交互。
项目根目录中存在gradlew
和文件是使用 Gradle 的明确标志。gradlew.bat
Gradle 项目将类似于以下内容:
project
├── gradle // (1)
│ ├── libs.versions.toml // (2)
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew // (3)
├── gradlew.bat // (3)
├── settings.gradle(.kts) // (4)
├── subproject-a
│ ├── build.gradle(.kts) // (5)
│ └── src // (6)
└── subproject-b
├── build.gradle(.kts) // (5)
└── src // (6)
-
Gradle 目录用于存储包装文件等
-
用于依赖管理的 Gradle 版本目录
-
Gradle 包装脚本
-
Gradle 设置文件用于定义根项目名称和子项目
-
两个子项目的 Gradle 构建脚本 -
subproject-a
以及subproject-b
-
项目的源代码和/或附加文件
调用Gradle
集成开发环境
Gradle内置于许多 IDE 中,包括 Android Studio、IntelliJ IDEA、Visual Studio Code、Eclipse 和 NetBeans。
当您在 IDE 中构建、清理或运行应用程序时,可以自动调用 Gradle。
建议您查阅所选 IDE 的手册,以了解有关如何使用和配置 Gradle 的更多信息。
Gradle 包装器
Wrapper 是一个调用 Gradle 声明版本的脚本,是执行 Gradle 构建的推荐方法。它可以在项目根目录中作为gradlew
或gradlew.bat
文件找到:
$ gradlew build // Linux or OSX
$ gradlew.bat build // Windows
下一步: 了解 Gradle 包装器>>
Gradle 包装器基础知识
执行任何 Gradle 构建的推荐方法是使用 Gradle Wrapper。
Wrapper脚本调用声明的 Gradle 版本,并在必要时提前下载它。
包装器可作为gradlew
或gradlew.bat
文件使用。
Wrapper 具有以下优点:
-
在给定 Gradle 版本上标准化项目。
-
为不同用户提供相同的 Gradle 版本。
-
为不同的执行环境(IDE、CI 服务器……)配置 Gradle 版本。
使用 Gradle 包装器
始终建议使用 Wrapper 执行构建,以确保构建的可靠、受控和标准化执行。
根据操作系统的不同,您可以运行gradlew
或gradlew.bat
来代替该gradle
命令。
典型的 Gradle 调用:
$ gradle build
要在 Linux 或 OSX 计算机上运行 Wrapper:
$ ./gradlew build
要在 Windows PowerShell 上运行包装器:
$ .\gradlew.bat build
该命令在 Wrapper 所在的同一目录中运行。如果要在不同目录中运行该命令,则必须提供 Wrapper 的相对路径:
$ ../gradlew build
以下控制台输出演示了在 Windows 计算机上的命令提示符 (cmd) 中,对于基于 Java 的项目如何使用 Wrapper:
$ gradlew.bat build Downloading https://services.gradle.org/distributions/gradle-5.0-all.zip ..................................................................................... Unzipping C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0-all.zip to C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-al\ac27o8rbd0ic8ih41or9l32mv Set executable permissions for: C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0\bin\gradle BUILD SUCCESSFUL in 12s 1 actionable task: 1 executed
请参阅Gradle Wrapper 参考以了解更多信息。
下一步: 了解 Gradle CLI >>
命令行界面基础知识
命令行界面是在 IDE 之外与 Gradle 交互的主要方法。
强烈鼓励使用Gradle Wrapper 。
在以下示例中替换为./gradlew
(在 macOS / Linux 中)或gradlew.bat
(在 Windows 中) 。gradle
在命令行执行Gradle符合以下结构:
gradle [taskName...] [--option-name...]
任务名称之前和之后允许使用选项。
gradle [--option-name...] [taskName...]
如果指定了多个任务,则应该用空格分隔它们。
gradle [taskName1 taskName2...] [--option-name...]
接受值的选项可以=
在选项和参数之间指定,也可以不指定。=
推荐使用。
gradle [...] --console=plain
启用行为的选项具有长格式选项,其逆值用 指定--no-
。以下是相反的情况。
gradle [...] --build-cache gradle [...] --no-build-cache
许多长期权都有相应的短期权。以下是等效的:
gradle --help gradle -h
设置文件基础知识
设置文件是每个 Gradle 项目的入口点。
设置文件的主要目的是将子项目添加到您的构建中。
Gradle 支持单项目和多项目构建。
-
对于单项目构建,设置文件是可选的。
-
对于多项目构建,设置文件是必需的并声明所有子项目。
设置脚本
设置文件是一个脚本。它可以是settings.gradle
用 Groovy 编写的文件,也可以是settings.gradle.kts
用 Kotlin 编写的文件。
Groovy DSL和Kotlin DSL是 Gradle 脚本唯一接受的语言。
设置文件通常位于项目的根目录中。
让我们看一个例子并将其分解:
rootProject.name = "root-project" // (1)
include("sub-project-a") // (2)
include("sub-project-b")
include("sub-project-c")
-
Define the project name.
-
Add subprojects.
rootProject.name = 'root-project' // (1)
include('sub-project-a') // (2)
include('sub-project-b')
include('sub-project-c')
-
Define the project name.
-
Add subprojects.
构建文件基础知识
一般来说,构建脚本详细介绍了构建配置、任务和插件。
每个 Gradle 构建至少包含一个构建脚本。
在构建文件中,可以添加两种类型的依赖项:
-
Gradle 和构建脚本所依赖的库和/或插件。
-
项目源(即源代码)所依赖的库。
构建脚本
构建脚本可以是build.gradle
用 Groovy 编写的文件,也可以是build.gradle.kts
用 Kotlin 编写的文件。
Groovy DSL和Kotlin DSL是 Gradle 脚本唯一接受的语言。
让我们看一个例子并将其分解:
plugins {
id("application") // (1)
}
application {
mainClass = "com.example.Main" // (2)
}
-
Add plugins.
-
Use convention properties.
plugins {
id 'application' // (1)
}
application {
mainClass = 'com.example.Main' // (2)
}
-
Add plugins.
-
Use convention properties.
1.添加插件
插件扩展了 Gradle 的功能,并且可以向项目贡献任务。
将插件添加到构建中称为应用插件,并使附加功能可用。
plugins {
id("application")
}
该application
插件有助于创建可执行的 JVM 应用程序。
依赖管理基础知识
Gradle 内置了对依赖管理的支持。
依赖管理是一种用于声明和解析项目所需的外部资源的自动化技术。
Gradle 构建脚本定义了构建可能需要外部依赖项的项目的过程。依赖项是指支持构建项目的 JAR、插件、库或源代码。
版本目录
版本目录提供了一种将依赖项声明集中在libs.versions.toml
文件中的方法。
该目录使子项目之间共享依赖关系和版本配置变得简单。它还允许团队在大型项目中强制执行库和插件的版本。
版本目录通常包含四个部分:
-
[versions] 声明插件和库将引用的版本号。
-
[libraries] 定义构建文件中使用的库。
-
[bundles] 定义一组依赖项。
-
[plugins] 定义插件。
[versions]
androidGradlePlugin = "7.4.1"
mockito = "2.16.0"
[libraries]
googleMaterial = { group = "com.google.android.material", name = "material", version = "1.1.0-alpha05" }
mockitoCore = { module = "org.mockito:mockito-core", version.ref = "mockito" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "androidGradlePlugin" }
该文件位于该gradle
目录中,以便 Gradle 和 IDE 自动使用它。应将版本目录签入源代码管理:gradle/libs.versions.toml
。
声明你的依赖关系
要向项目添加依赖项,请在build.gradle(.kts)
文件的依赖项块中指定依赖项。
以下build.gradle.kts
文件使用上面的版本目录向项目添加一个插件和两个依赖项:
plugins {
alias(libs.plugins.androidApplication) // (1)
}
dependencies {
// Dependency on a remote binary to compile and run the code
implementation(libs.googleMaterial) // (2)
// Dependency on a remote binary to compile and run the test code
testImplementation(libs.mockitoCore) // (3)
}
-
将 Android Gradle 插件应用于此项目,该项目添加了一些特定于构建 Android 应用程序的功能。
-
将材质依赖项添加到项目中。 Material Design 提供了用于在 Android 应用程序中创建用户界面的组件。该库将用于编译和运行本项目中的 Kotlin 源代码。
-
将 Mockito 依赖项添加到项目中。 Mockito 是一个用于测试 Java 代码的模拟框架。该库将用于编译和运行该项目中的测试源代码。
Gradle 中的依赖项按配置进行分组。
-
该
material
库被添加到implementation
配置中,用于编译和运行生产代码。 -
该
mockito-core
库被添加到testImplementation
配置中,用于编译和运行测试代码。
笔记
|
还有更多配置可供选择。 |
查看项目依赖关系
您可以使用以下命令在终端中查看依赖关系树./gradlew :app:dependencies
:
$ ./gradlew :app:dependencies
> Task :app:dependencies
------------------------------------------------------------
Project ':app'
------------------------------------------------------------
implementation - Implementation only dependencies for source set 'main'. (n)
\--- com.google.android.material:material:1.1.0-alpha05 (n)
testImplementation - Implementation only dependencies for source set 'test'. (n)
\--- org.mockito:mockito-core:2.16.0 (n)
...
请参阅依赖管理章节以了解更多信息。
下一步: 了解任务>>
任务基础知识
任务代表构建执行的某些独立工作单元,例如编译类、创建 JAR、生成 Javadoc 或将存档发布到存储库。
build
您可以使用以下命令或通过调用项目目录中的gradle
Gradle Wrapper(./gradlew
或)来运行 Gradle任务:gradlew.bat
$ ./gradlew build
可用任务
项目中的所有可用任务都来自 Gradle 插件和构建脚本。
您可以通过在终端中运行以下命令来列出项目中的所有可用任务:
$ ./gradlew tasks
Application tasks
-----------------
run - Runs this project as a JVM application
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
...
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
...
Other tasks
-----------
compileJava - Compiles main Java source.
...
运行任务
该run
任务的执行方式为./gradlew run
:
$ ./gradlew run
> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes
> Task :app:run
Hello World!
BUILD SUCCESSFUL in 904ms
2 actionable tasks: 2 executed
在此示例 Java 项目中,任务的输出run
是Hello World
打印在控制台上的语句。
任务依赖性
很多时候,一个任务需要先运行另一个任务。
例如,要让 Gradle 执行build
任务,必须首先编译 Java 代码。因此,build
任务取决于任务compileJava
。
这意味着该任务将在以下任务之前compileJava
运行:build
$ ./gradlew build
> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes
> Task :app:jar
> Task :app:startScripts
> Task :app:distTar
> Task :app:distZip
> Task :app:assemble
> Task :app:compileTestJava
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses
> Task :app:test
> Task :app:check
> Task :app:build
BUILD SUCCESSFUL in 764ms
7 actionable tasks: 7 executed
构建脚本可以选择定义任务依赖关系。然后 Gradle 自动确定任务执行顺序。
请参阅任务开发章节以了解更多信息。
下一步: 了解插件>>
插件基础知识
Gradle 是建立在插件系统之上的。 Gradle 本身主要由基础设施组成,例如复杂的依赖解析引擎。它的其余功能来自插件。
插件是一个为 Gradle 构建系统提供附加功能的软件。
插件可以应用于 Gradle 构建脚本以添加新任务、配置或其他与构建相关的功能:
- Java 库插件 -
java-library
-
用于定义和构建 Java 库。它随任务编译 Java 源代码
compileJava
,随任务生成 Javadocjavadoc
,并随任务将编译的类打包到 JAR 文件中jar
。 - Google 服务 Gradle 插件 -
com.google.gms:google-services
-
使用名为 的配置块
googleServices{}
和名为 的任务在 Android 应用程序中启用 Google API 和 Firebase 服务generateReleaseAssets
。 - Gradle Bintray 插件 -
com.jfrog.bintray
-
允许您通过使用块配置插件来将工件发布到 Bintray
bintray{}
。
插件分发
插件以三种方式分发:
-
核心插件- Gradle 开发并维护一组核心插件。
-
社区插件- Gradle 社区通过Gradle 插件门户共享插件。
-
本地插件- Gradle 使用户能够使用API创建自定义插件。
应用插件
将插件应用到项目允许插件扩展项目的功能。
您可以使用插件 ID(全局唯一标识符/名称)和版本在构建脚本中应用插件:
plugins {
id «plugin id» version «plugin version»
}
1. 核心插件
Gradle Core 插件是 Gradle 发行版本身包含的一组插件。这些插件提供了构建和管理项目的基本功能。
核心插件的一些示例包括:
-
java:提供对构建 Java 项目的支持。
-
groovy:添加对编译和测试 Groovy 源文件的支持。
-
ear:添加了对为企业应用程序构建 EAR 文件的支持。
核心插件的独特之处在于,它们在构建脚本中应用时提供短名称,例如java
核心JavaPlugin 。它们也不需要版本。要将java
插件应用到项目中:
plugins {
id("java")
}
用户可以利用许多Gradle 核心插件。
2. 社区插件
社区插件是由 Gradle 社区开发的插件,而不是核心 Gradle 发行版的一部分。这些插件提供可能特定于某些用例或技术的附加功能。
Spring Boot Gradle 插件打包可执行 JAR 或 WAR 档案,并运行Spring Boot Java 应用程序。
要将org.springframework.boot
插件应用到项目中:
plugins {
id("org.springframework.boot") version "3.1.5"
}
社区插件可以在Gradle 插件门户上发布,其他 Gradle 用户可以轻松发现和使用它们。
3.本地插件
自定义或本地插件是在特定项目或组织内开发和使用的。这些插件不会公开共享,而是根据项目或组织的特定需求量身定制的。
本地插件可以封装常见的构建逻辑,提供与内部系统或工具的集成,或者将复杂的功能抽象为可重用的组件。
Gradle 为用户提供了使用 API 开发自定义插件的能力。要创建您自己的插件,您通常需要遵循以下步骤:
-
定义插件类:创建一个实现接口的新类
Plugin<Project>
。// Define a 'HelloPlugin' plugin class HelloPlugin : Plugin<Project> { override fun apply(project: Project) { // Define the 'hello' task val helloTask = project.tasks.register("hello") { doLast { println("Hello, Gradle!") } } } }
-
构建并可选择发布您的插件:生成一个包含插件代码的 JAR 文件,并可选择将此 JAR 发布到存储库(本地或远程)以在其他项目中使用。
// Publish the plugin plugins { `maven-publish` } publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) } } repositories { mavenLocal() } }
-
应用您的插件:当您想要使用插件时,请在
plugins{}
构建文件的块中包含插件 ID 和版本。// Apply the plugin plugins { id("com.example.hello") version "1.0" }
请参阅插件开发章节以了解更多信息。
下一步: 了解增量构建和构建缓存>>
Gradle 增量构建和构建缓存
Gradle 使用两个主要功能来减少构建时间:增量构建和构建缓存。
增量构建
增量构建是一种避免运行自上次构建以来输入未更改的任务的构建。如果这些任务只会重新产生相同的输出,则无需重新执行这些任务。
为了使增量构建发挥作用,任务必须定义其输入和输出。 Gradle 将确定输入或输出在构建时是否已更改。如果它们发生了变化,Gradle 将执行任务。否则,它将跳过执行。
增量构建始终处于启用状态,查看它们实际效果的最佳方法是打开详细模式。在详细模式下,每个任务状态在构建期间都会被标记:
$ ./gradlew compileJava --console=verbose
> Task :buildSrc:generateExternalPluginSpecBuilders UP-TO-DATE
> Task :buildSrc:extractPrecompiledScriptPluginPlugins UP-TO-DATE
> Task :buildSrc:compilePluginsBlocks UP-TO-DATE
> Task :buildSrc:generatePrecompiledScriptPluginAccessors UP-TO-DATE
> Task :buildSrc:generateScriptPluginAdapters UP-TO-DATE
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :list:compileJava UP-TO-DATE
> Task :utilities:compileJava UP-TO-DATE
> Task :app:compileJava UP-TO-DATE
BUILD SUCCESSFUL in 374ms
12 actionable tasks: 12 up-to-date
当您运行先前已执行且未更改的任务时,UP-TO-DATE
任务旁边会打印 then 。
提示
|
要永久启用详细模式,请添加org.gradle.console=verbose 到您的gradle.properties 文件中。
|
构建缓存
增量构建是一种很好的优化,有助于避免已经完成的工作。如果开发人员不断更改单个文件,则可能不需要重建项目中的所有其他文件。
但是,当同一个开发人员切换到上周创建的新分支时会发生什么?即使开发人员正在构建以前构建过的东西,文件也会被重建。
这就是构建缓存有用的地方。
构建缓存存储以前的构建结果并在需要时恢复它们。它可以防止执行耗时且昂贵的流程的冗余工作和成本。
当构建缓存已用于重新填充本地目录时,任务将标记为FROM-CACHE
:
$ ./gradlew compileJava --build-cache
> Task :buildSrc:generateExternalPluginSpecBuilders UP-TO-DATE
> Task :buildSrc:extractPrecompiledScriptPluginPlugins UP-TO-DATE
> Task :buildSrc:compilePluginsBlocks UP-TO-DATE
> Task :buildSrc:generatePrecompiledScriptPluginAccessors UP-TO-DATE
> Task :buildSrc:generateScriptPluginAdapters UP-TO-DATE
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :list:compileJava FROM-CACHE
> Task :utilities:compileJava FROM-CACHE
> Task :app:compileJava FROM-CACHE
BUILD SUCCESSFUL in 364ms
12 actionable tasks: 3 from cache, 9 up-to-date
重新填充本地目录后,下一次执行会将任务标记为UP-TO-DATE
而不是FROM-CACHE
。
构建缓存允许您跨团队共享和重用未更改的构建和测试输出。这加快了本地和 CI 构建的速度,因为不会浪费周期来重新构建不受新代码更改影响的二进制文件。
请参阅“构建缓存”一章以了解更多信息。
下一步: 了解构建扫描>>
其他主题
持续构建
持续构建允许您在文件输入更改时自动重新执行请求的任务。您可以使用-t
或--continuous
命令行选项在此模式下执行构建。
例如,您可以test
通过运行以下命令连续运行该任务和所有相关任务:
$ gradle test --continuous
gradle test
Gradle 的行为就像您在更改有助于请求任务的源或测试后运行一样。这意味着不相关的更改(例如对构建脚本的更改)将不会触发重建。要合并构建逻辑更改,必须手动重新启动连续构建。
持续构建使用文件系统监视来检测输入的更改。如果文件系统监视在您的系统上不起作用,那么连续构建也将不起作用。特别是,使用时连续构建不起作用--no-daemon
。
当 Gradle 检测到输入发生更改时,它不会立即触发构建。相反,它将等待一段时间(安静期),直到没有检测到任何其他更改。您可以通过 Gradle 属性以毫秒为单位配置安静期org.gradle.continuous.quietperiod
。
终止持续构建
如果 Gradle 连接到交互式输入源(例如终端),则可以通过按退出连续构建CTRL-D
(在 Microsoft Windows 上,还需要按ENTER
或RETURN
之后CTRL-D
)。
如果 Gradle 未附加到交互式输入源(例如作为脚本的一部分运行),则必须终止构建过程(例如使用命令kill
或类似命令)。
如果通过 Tooling API 执行构建,则可以使用 Tooling API 的取消机制取消构建。
局限性
在某些情况下,连续构建可能无法检测输入的更改。
创建输入目录
有时,由于文件系统监视的工作方式,创建以前丢失的输入目录不会触发构建。例如,创建src/main/java
目录可能不会触发构建。同样,如果输入是经过筛选的文件树,并且没有文件与筛选器匹配,则匹配文件的创建可能不会触发构建。
对项目目录之外的文件进行更改
Gradle 仅监视项目目录内文件的更改。对项目目录外部文件的更改将不会被检测到,并且不会触发构建。
构建周期
Gradle 在任务执行之前开始监视更改。如果任务在执行时修改了自己的输入,Gradle 将检测到更改并触发新的构建。如果每次任务执行时,再次修改输入,则会再次触发构建。这并不是持续构建所独有的。在没有持续构建的情况下“正常”运行时,修改其自身输入的任务永远不会被认为是最新的。
如果您的构建进入这样的构建周期,您可以通过查看 Gradle 报告的更改的文件列表来跟踪任务。识别出每次构建期间更改的文件后,您应该查找将该文件作为输入的任务。在某些情况下,这可能是显而易见的(例如,Java 文件是用 编译的compileJava
)。在其他情况下,您可以使用--info
日志记录来查找由于识别的文件而过期的任务。
基础
摇篮目录
Gradle 使用两个主要目录来执行和管理其工作:Gradle 用户主目录和项目根目录。
Gradle 用户主目录
默认情况下,Gradle 用户主页(~/.gradle
或C:\Users\<USERNAME>\.gradle
)存储全局配置属性、初始化脚本、缓存和日志文件。
可以通过环境变量来设置GRADLE_USER_HOME
。
提示
|
GRADLE_HOME 不要与Gradle 的可选安装目录
混淆。 |
其结构大致如下:
├── caches // (1) │ ├── 4.8 // (2) │ ├── 4.9 // (2) │ ├── ⋮ │ ├── jars-3 // (3) │ └── modules-2 // (3) ├── daemon // (4) │ ├── ⋮ │ ├── 4.8 │ └── 4.9 ├── init.d // (5) │ └── my-setup.gradle ├── jdks // (6) │ ├── ⋮ │ └── jdk-14.0.2+12 ├── wrapper │ └── dists // (7) │ ├── ⋮ │ ├── gradle-4.8-bin │ ├── gradle-4.9-all │ └── gradle-4.9-bin └── gradle.properties // (8)
-
全局缓存目录(用于所有非项目特定的内容)。
-
版本特定的缓存(例如,支持增量构建)。
-
共享缓存(例如,用于依赖项的工件)。
-
Gradle Daemon的注册表和日志。
-
全局初始化脚本。
-
通过工具链支持下载的 JDK 。
-
由Gradle Wrapper下载的发行版。
-
全局Gradle 配置属性。
请参阅Gradle 目录参考以了解更多信息。
项目根目录
项目根目录包含项目中的所有源文件。
它还包含 Gradle 生成的文件和目录,例如.gradle
和build
。
虽然.gradle
通常会签入源代码管理,但该build
目录包含构建的输出以及 Gradle 用于支持增量构建等功能的临时文件。
典型项目根目录的剖析如下:
├── .gradle // (1) │ ├── 4.8 // (2) │ ├── 4.9 // (2) │ └── ⋮ ├── build // (3) ├── gradle │ └── wrapper // (4) ├── gradle.properties // (5) ├── gradlew // (6) ├── gradlew.bat // (6) ├── settings.gradle.kts // (7) ├── subproject-one // (8) | └── build.gradle.kts // (9) ├── subproject-two // (8) | └── build.gradle.kts // (9) └── ⋮
-
Gradle 生成的项目特定的缓存目录。
-
版本特定的缓存(例如,支持增量构建)。
-
该项目的构建目录,Gradle 在其中生成所有构建工件。
-
包含Gradle Wrapper的 JAR 文件和配置。
-
项目特定的Gradle 配置属性。
-
使用Gradle Wrapper执行构建的脚本。
-
定义子项目列表的项目设置文件。
-
通常,一个项目被组织成一个或多个子项目。
-
每个子项目都有自己的 Gradle 构建脚本。
请参阅Gradle 目录参考以了解更多信息。
下一步: 了解如何构建多项目构建>>
多项目构建基础知识
Gradle 支持多项目构建。
虽然一些小型项目和整体应用程序可能包含单个构建文件和源代码树,但更常见的情况是项目被分割成更小的、相互依赖的模块。 “相互依赖”这个词至关重要,因为您通常希望通过单个构建将许多模块链接在一起。
Gradle 通过多项目构建支持这种场景。这有时被称为多模块项目。 Gradle 将模块称为子项目。
多项目构建由一个根项目和一个或多个子项目组成。
多项目结构
以下表示包含两个子项目的多项目构建的结构:
目录结构应如下所示:
├── .gradle │ └── ⋮ ├── gradle │ ├── libs.version.toml │ └── wrapper ├── gradlew ├── gradlew.bat ├── settings.gradle.kts // (1) ├── sub-project-1 │ └── build.gradle.kts // (2) ├── sub-project-2 │ └── build.gradle.kts // (2) └── sub-project-3 └── build.gradle.kts // (2)
-
该
settings.gradle.kts
文件应包含所有子项目。 -
每个子项目都应该有自己的
build.gradle.kts
文件。
多项目标准
Gradle 社区对于多项目构建结构有两个标准:
-
使用 buildSrc 进行多项目构建- 其中
buildSrc
是 Gradle 项目根目录中包含所有构建逻辑的类似子项目的目录。 -
复合构建- 包含其他构建的构建,其中
build-logic
包含可重用构建逻辑的 Gradle 项目根目录中的构建目录。
1. 多项目构建使用buildSrc
多项目构建允许您组织具有许多模块的项目,连接这些模块之间的依赖关系,并轻松地在它们之间共享通用的构建逻辑。
例如,一个包含许多名为mobile-app
、web-app
、api
、lib
、的模块的构建documentation
可以按如下方式构建:
.
├── gradle
├── gradlew
├── settings.gradle.kts
├── buildSrc
│ ├── build.gradle.kts
│ └── src/main/kotlin/shared-build-conventions.gradle.kts
├── mobile-app
│ └── build.gradle.kts
├── web-app
│ └── build.gradle.kts
├── api
│ └── build.gradle.kts
├── lib
│ └── build.gradle.kts
└── documentation
└── build.gradle.kts
这些模块之间将具有依赖关系,例如web-app
和mobile-app
dependent on lib
。这意味着为了让 Gradle 构建web-app
或mobile-app
,它必须lib
首先构建。
在此示例中,根设置文件将如下所示:
include("mobile-app", "web-app", "api", "lib", "documentation")
笔记
|
子项目(模块)的包含顺序并不重要。 |
buildSrc
Gradle 会自动识别该目录。它是定义和维护共享配置或命令式构建逻辑(例如自定义任务或插件)的好地方。
buildSrc
如果build.gradle(.kts)
在buildSrc
.
如果java
插件应用于buildSrc
项目,则编译后的代码buildSrc/src/main/java
将放入根构建脚本的类路径中,使其可用于构建中的任何子项目( web-app
、mobile-app
、等...)。lib
请参阅如何声明子项目之间的依赖关系以了解更多信息。
2. 复合构建
复合构建,也称为包含构建,最适合在构建(而不是子项目)之间共享逻辑或隔离对共享构建逻辑(即约定插件)的访问。
让我们以前面的例子为例。其中的逻辑buildSrc
已转变为包含插件的项目,并且可以独立于根项目构建进行发布和工作。
build-logic
该插件被移动到使用构建脚本和设置文件调用的自己的构建中:
.
├── gradle
├── gradlew
├── settings.gradle.kts
├── build-logic
│ ├── settings.gradle.kts
│ └── conventions
│ ├── build.gradle.kts
│ └── src/main/kotlin/shared-build-conventions.gradle.kts
├── mobile-app
│ └── build.gradle.kts
├── web-app
│ └── build.gradle.kts
├── api
│ └── build.gradle.kts
├── lib
│ └── build.gradle.kts
└── documentation
└── build.gradle.kts
笔记
|
build-logic 位于根项目的子目录中
的事实是无关紧要的。如果需要,该文件夹可以位于根项目之外。
|
根设置文件包括整个build-logic
构建:
pluginManagement {
includeBuild("build-logic")
}
include("mobile-app", "web-app", "api", "lib", "documentation")
请参阅如何创建复合构建以includeBuild
了解更多信息。
多项目路径
项目路径具有以下模式:它以可选的冒号开头,表示根项目。
根项目:
是路径中唯一未按其名称指定的项目。
项目路径的其余部分是用冒号分隔的项目名称序列,其中下一个项目是上一个项目的子项目:
:sub-project-1
运行时可以看到项目路径gradle projects
:
------------------------------------------------------------
Root project 'project'
------------------------------------------------------------
Root project 'project'
+--- Project ':sub-project-1'
\--- Project ':sub-project-2'
项目路径通常反映文件系统布局,但也有例外。最显着的是复合构建。
确定项目结构
您可以使用该gradle projects
命令来识别项目结构。
作为示例,让我们使用具有以下结构的多项目构建:
> gradle -q projects
------------------------------------------------------------ Root project 'multiproject' ------------------------------------------------------------ Root project 'multiproject' +--- Project ':api' +--- Project ':services' | +--- Project ':services:shared' | \--- Project ':services:webservice' \--- Project ':shared' To see a list of the tasks of a project, run gradle <project-path>:tasks For example, try running gradle :api:tasks
多项目构建是您可以运行的任务的集合。不同之处在于您可能想要控制执行哪个项目的任务。
以下部分将介绍在多项目构建中执行任务的两个选项。
按名称执行任务
该命令将在相对于具有该任务的当前工作目录的任何子项目中gradle test
执行该任务。test
如果从项目根目录运行该命令,您将test
在api、shared、services:shared和services:webservice中运行。
如果从services项目目录运行命令,则只会执行services:shared和services:webservice中的任务。
Gradle 行为背后的基本规则是使用该名称执行层次结构中的所有任务。如果在遍历的任何子项目中都没有找到这样的任务,则进行抱怨。
笔记
|
某些任务选择器(例如help 或dependencies )只会在调用它们的项目上运行任务,而不是在所有子项目上运行,以减少屏幕上打印的信息量。
|
通过完全限定名称执行任务
您可以使用任务的完全限定名称来执行特定子项目中的特定任务。例如:gradle :services:webservice:build
将运行webservicebuild
子项目的任务。
任务的完全限定名称是其项目路径加上任务名称。
这种方法适用于任何任务,因此如果您想知道特定子项目中有哪些任务,请使用任务tasks
,例如gradle :services:webservice:tasks
。
多项目构建和测试
该build
任务通常用于编译、测试和检查单个项目。
在多项目构建中,您可能经常希望跨多个项目执行所有这些任务。和buildNeeded
任务buildDependents
可以帮助解决这个问题。
在此示例中,:services:person-service
项目同时依赖于:api
和:shared
项目。项目:api
也要看:shared
项目。
假设您正在处理单个项目,即该:api
项目,您一直在进行更改,但自从执行clean
.您想要构建任何必要的支持 JAR,但仅对已更改的项目部分执行代码质量和单元测试。
该build
任务执行以下操作:
$ gradle :api:build > Task :shared:compileJava > Task :shared:processResources > Task :shared:classes > Task :shared:jar > Task :api:compileJava > Task :api:processResources > Task :api:classes > Task :api:jar > Task :api:assemble > Task :api:compileTestJava > Task :api:processTestResources > Task :api:testClasses > Task :api:test > Task :api:check > Task :api:build BUILD SUCCESSFUL in 0s
如果您刚刚从版本控制系统获取了最新版本的源代码,其中包括:api
依赖的其他项目中的更改,您可能想要构建您依赖的所有项目并测试它们。
该buildNeeded
任务构建并测试配置的项目依赖项中的所有项目testRuntime
:
$ gradle :api:buildNeeded > Task :shared:compileJava > Task :shared:processResources > Task :shared:classes > Task :shared:jar > Task :api:compileJava > Task :api:processResources > Task :api:classes > Task :api:jar > Task :api:assemble > Task :api:compileTestJava > Task :api:processTestResources > Task :api:testClasses > Task :api:test > Task :api:check > Task :api:build > Task :shared:assemble > Task :shared:compileTestJava > Task :shared:processTestResources > Task :shared:testClasses > Task :shared:test > Task :shared:check > Task :shared:build > Task :shared:buildNeeded > Task :api:buildNeeded BUILD SUCCESSFUL in 0s
您可能想要重构:api
其他项目中使用的项目的某些部分。如果进行这些更改,仅测试:api
项目是不够的。您必须测试依赖于该:api
项目的所有项目。
该buildDependents
任务测试对指定项目具有项目依赖性(在 testRuntime 配置中)的所有项目:
$ gradle :api:buildDependents > Task :shared:compileJava > Task :shared:processResources > Task :shared:classes > Task :shared:jar > Task :api:compileJava > Task :api:processResources > Task :api:classes > Task :api:jar > Task :api:assemble > Task :api:compileTestJava > Task :api:processTestResources > Task :api:testClasses > Task :api:test > Task :api:check > Task :api:build > Task :services:person-service:compileJava > Task :services:person-service:processResources > Task :services:person-service:classes > Task :services:person-service:jar > Task :services:person-service:assemble > Task :services:person-service:compileTestJava > Task :services:person-service:processTestResources > Task :services:person-service:testClasses > Task :services:person-service:test > Task :services:person-service:check > Task :services:person-service:build > Task :services:person-service:buildDependents > Task :api:buildDependents BUILD SUCCESSFUL in 0s
最后,您可以构建和测试所有项目中的所有内容。您在根项目文件夹中运行的任何任务都将导致该同名任务在所有子项目上运行。
您可以运行gradle build
来构建和测试所有项目。
请参阅构建构建章节以了解更多信息。
下一步: 了解 Gradle 构建生命周期>>
构建生命周期
作为构建作者,您可以定义任务以及任务之间的依赖关系。 Gradle 保证这些任务将按照其依赖关系的顺序执行。
您的构建脚本和插件配置此依赖关系图。
例如,如果您的项目任务包括build
、assemble
、createDocs
,您的构建脚本可以确保它们按照build
→ assemble
→ 的顺序执行createDoc
。
任务图
Gradle 在执行任何任务之前构建任务图。
在构建中的所有项目中,任务形成有向无环图(DAG)。
该图显示了两个示例任务图,一个是抽象的,另一个是具体的,任务之间的依赖关系用箭头表示:
插件和构建脚本都通过任务依赖机制和带注释的输入/输出对任务图做出贡献。
构建阶段
Gradle 构建具有三个不同的阶段。
Gradle 按顺序运行这些阶段:
- 第 1 阶段. 初始化
- 第 2 阶段. 配置
-
-
build.gradle(.kts)
评估参与构建的每个项目的构建脚本。 -
为请求的任务创建任务图。
-
- 第三阶段:执行
-
-
安排并执行选定的任务。
-
任务之间的依赖关系决定了执行顺序。
-
任务的执行可以并行发生。
-
例子
以下示例显示了设置和构建文件的哪些部分对应于各个构建阶段:
rootProject.name = "basic"
println("This is executed during the initialization phase.")
println("This is executed during the configuration phase.")
tasks.register("configured") {
println("This is also executed during the configuration phase, because :configured is used in the build.")
}
tasks.register("test") {
doLast {
println("This is executed during the execution phase.")
}
}
tasks.register("testBoth") {
doFirst {
println("This is executed first during the execution phase.")
}
doLast {
println("This is executed last during the execution phase.")
}
println("This is executed during the configuration phase as well, because :testBoth is used in the build.")
}
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
println 'This is executed during the configuration phase.'
tasks.register('configured') {
println 'This is also executed during the configuration phase, because :configured is used in the build.'
}
tasks.register('test') {
doLast {
println 'This is executed during the execution phase.'
}
}
tasks.register('testBoth') {
doFirst {
println 'This is executed first during the execution phase.'
}
doLast {
println 'This is executed last during the execution phase.'
}
println 'This is executed during the configuration phase as well, because :testBoth is used in the build.'
}
以下命令执行上面指定的test
和testBoth
任务。由于 Gradle 仅配置请求的任务及其依赖项,因此configured
任务从不配置:
> gradle test testBoth
This is executed during the initialization phase.
> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well, because :testBoth is used in the build.
> Task :test
This is executed during the execution phase.
> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.
BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
> gradle test testBoth
This is executed during the initialization phase.
> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well, because :testBoth is used in the build.
> Task :test
This is executed during the execution phase.
> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.
BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
第 1 阶段. 初始化
在初始化阶段,Gradle 检测项目集(根项目和子项目)并包含参与构建的构建。
Gradle 首先评估设置文件,settings.gradle(.kts)
并实例化一个Settings
对象。然后,Gradle 实例化Project
每个项目的实例。
第 2 阶段. 配置
在配置阶段,Gradle 将任务和其他属性添加到初始化阶段找到的项目中。
写入设置文件
设置文件是每个 Gradle 构建的入口点。
找到设置文件后settings.gradle(.kts)
,Gradle 会实例化一个Settings
对象。
该对象的用途之一Settings
是允许您声明要包含在构建中的所有项目。
设置脚本
设置脚本可以是settings.gradle
Groovy 中的文件,也可以是settings.gradle.kts
Kotlin 中的文件。
在 Gradle 组装项目进行构建之前,它会创建一个Settings
实例并针对它执行设置文件。
当设置脚本执行时,它会配置此Settings
.因此,设置文件定义了该Settings
对象。
重要的
|
Settings 实例和文件
之间存在一一对应的关系settings.gradle(.kts) 。
|
物体Settings
该对象是Gradle APISettings
的一部分。
设置脚本中的许多顶级属性和块都是设置 API 的一部分。
例如,我们可以使用以下属性在设置脚本中设置根项目名称Settings.rootProject
:
settings.rootProject.name = "root"
通常缩写为:
rootProject.name = "root"
标准Settings
特性
该Settings
对象在您的设置脚本中公开一组标准属性。
下表列出了一些常用的属性:
姓名 | 描述 |
---|---|
|
构建缓存配置。 |
|
已应用于设置的插件的容器。 |
|
构建的根目录。根目录是根项目的项目目录。 |
|
构建的根项目。 |
|
返回此设置对象。 |
下表列出了几种常用的方法:
姓名 | 描述 |
---|---|
|
将给定的项目添加到构建中。 |
|
包括复合构建的指定路径处的构建。 |
设置脚本结构
设置脚本是对 Gradle API 的一系列方法调用,通常使用{ … }
,这是 Groovy 和 Kotlin 语言中的特殊快捷方式。块在 Kotlin 中{ }
称为lambda ,在 Groovy 中称为闭包。
简而言之,该plugins{ }
块是一个方法调用,其中传递 Kotlin lambda对象或 Groovy闭包对象作为参数。它的缩写形式是:
plugins(function() {
id("plugin")
})
块映射到 Gradle API 方法。
函数内的代码针对一个对象执行,该对象在 Kotlin lambda 中this
称为接收器,在 Groovy 闭包中称为委托。 Gradle 确定正确的this
对象并调用正确的相应方法。this
方法调用对象的 的类型id("plugin")
为PluginDependenciesSpec
。
设置文件由构建在 DSL 之上的 Gradle API 调用组成。 Gradle 从上到下逐行执行脚本。
让我们看一个例子并将其分解:
pluginManagement { // (1)
repositories {
gradlePluginPortal()
google()
}
}
plugins { // (2)
id("org.gradle.toolchains.fake") version "0.6.0"
}
rootProject.name = "root-project" // (3)
dependencyResolutionManagement { // (4)
repositories {
mavenCentral()
}
}
include("sub-project-a") // (5)
include("sub-project-b")
include("sub-project-c")
-
Define the location of plugins
-
Apply plugins.
-
Define the root project name.
-
Define build-wide repositories.
-
Add subprojects to the build.
pluginManagement { // (1)
repositories {
gradlePluginPortal()
google()
}
}
plugins { // (2)
id 'org.gradle.toolchains.fake' version '0.6.0'
}
rootProject.name = 'root-project' // (3)
dependencyResolutionManagement { // (4)
repositories {
mavenCentral()
}
}
include('sub-project-a') // (5)
include('sub-project-b')
include('sub-project-c')
-
Define the location of plugins.
-
Apply plugins.
-
Define the root project name.
-
Define build-wide repositories.
-
Add subprojects to the build.
1.定义插件的位置
设置文件可以选择定义您的项目使用的插件pluginManagement
,包括二进制存储库,例如 Gradle 插件门户或其他使用以下 Gradle 构建includeBuild
:
pluginManagement {
repositories {
gradlePluginPortal()
google()
}
}
您还可以在此块中包含插件和插件依赖项解析策略。
2. 应用插件
设置文件可以选择声明稍后可能应用的插件,这可以在多个构建/子项目之间添加共享配置:
应用于设置的插件仅影响Settings
对象。
plugins {
id("org.gradle.toolchains.fake") version "0.6.0"
}
这通常用于确保所有子项目使用相同的插件版本。
4. 定义构建范围的存储库
设置文件可以选择使用 Maven Central 等二进制存储库和/或其他 Gradle 构建来定义项目所依赖的组件的位置(以及如何解析它们):repositories
includeBuild
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
您还可以在此部分中包含版本目录。
5. 将子项目添加到构建中
设置文件通过使用以下语句添加所有子项目来定义项目的结构include
:
include("app")
include("business-logic")
include("data-model")
设置文件脚本
该对象还有更多属性和方法Settings
可用于配置构建。
重要的是要记住,虽然许多 Gradle 脚本通常是用简短的 Groovy 或 Kotlin 语法编写的,但设置脚本中的每个项目本质上都是调用Settings
Gradle API 中对象的方法:
include("app")
实际上是:
settings.include("app")
此外,您还可以使用 Groovy 和 Kotlin 语言的全部功能。
例如,include
您可以迭代项目根文件夹中的目录列表并自动包含它们,而不是多次添加子项目:
rootDir.listFiles().filter { it.isDirectory && (new File(it, "build.gradle.kts").exists()) }.forEach {
include(it.name)
}
提示
|
这种类型的逻辑应该在插件中开发。 |
下一步: 学习如何编写构建脚本>>
编写构建脚本
Gradle Build 生命周期中的初始化阶段使用设置文件查找项目根目录中包含的根项目和子项目。
然后,对于设置文件中包含的每个项目,Gradle 都会创建一个Project
实例。
然后 Gradle 查找相应的构建脚本文件,该文件在配置阶段使用。
构建脚本
每个 Gradle 构建都包含一个或多个项目;根项目和子项目。
项目通常对应于需要构建的软件组件,例如库或应用程序。它可能代表一个库 JAR、一个 Web 应用程序或由其他项目生成的 JAR 组装而成的分发 ZIP。
另一方面,它可能代表要做的事情,例如将应用程序部署到临时或生产环境。
Gradle 脚本使用 Groovy DSL 或 Kotlin DSL(特定领域语言)编写。
构建脚本配置项目并与 类型的对象关联Project
。
当构建脚本执行时,它会配置Project
.
构建脚本可以是*.gradle
Groovy 中的文件,也可以是*.gradle.kts
Kotlin 中的文件。
重要的
|
构建脚本配置Project 对象及其子对象。
|
物体Project
该对象是Gradle APIProject
的一部分:
构建脚本中的许多顶级属性和块都是项目 API 的一部分。
例如,以下构建脚本使用Project.name属性来打印项目的名称:
println(name)
println(project.name)
println name
println project.name
$ gradle -q check project-api project-api
两个println
语句都打印出相同的属性。
name
第一个使用对象属性的顶级引用Project
。第二条语句使用project
可用于任何构建脚本的属性,该属性返回关联的Project
对象。
标准项目属性
该Project
对象在构建脚本中公开一组标准属性。
下表列出了一些常用的属性:
姓名 | 类型 | 描述 |
---|---|---|
|
|
项目目录的名称。 |
|
|
项目的完全限定名称。 |
|
|
项目的描述。 |
|
|
返回项目的依赖处理程序。 |
|
|
返回项目的存储库处理程序。 |
|
|
提供对项目的几个重要位置的访问。 |
|
|
这个项目的小组。 |
|
|
该项目的版本。 |
下表列出了几种常用的方法:
姓名 | 描述 |
---|---|
|
将文件路径解析为 URI,相对于该项目的项目目录。 |
|
使用给定名称创建一个任务并将其添加到此项目中。 |
构建脚本结构
{ … }
Build 脚本由Groovy 和 Kotlin 中的一个特殊对象组成。该对象在 Kotlin 中称为lambda ,在 Groovy 中称为闭包。
简而言之,该plugins{ }
块是一个方法调用,其中传递 Kotlin lambda对象或 Groovy闭包对象作为参数。它的缩写形式是:
plugins(function() {
id("plugin")
})
块映射到 Gradle API 方法。
函数内的代码针对一个对象执行,该对象在 Kotlin lambda 中this
称为接收器,在 Groovy 闭包中称为委托。 Gradle 确定正确的this
对象并调用正确的相应方法。this
方法调用对象的 的类型id("plugin")
为PluginDependenciesSpec
。
构建脚本本质上是由构建在 DSL 之上的 Gradle API 调用组成。 Gradle 从上到下逐行执行脚本。
让我们看一个例子并将其分解:
plugins { // (1)
id("org.jetbrains.kotlin.jvm") version "1.9.0"
id("application")
}
repositories { // (2)
mavenCentral()
}
dependencies { // (3)
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("com.google.guava:guava:32.1.1-jre")
}
application { // (4)
mainClass = "com.example.Main"
}
tasks.named<Test>("test") { // (5)
useJUnitPlatform()
}
-
Apply plugins to the build.
-
Define the locations where dependencies can be found.
-
Add dependencies.
-
Set properties.
-
Register and configure tasks.
plugins { // (1)
id 'org.jetbrains.kotlin.jvm' version '1.9.0'
id 'application'
}
repositories { // (2)
mavenCentral()
}
dependencies { // (3)
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.3'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'com.google.guava:guava:32.1.1-jre'
}
application { // (4)
mainClass = 'com.example.Main'
}
tasks.named('test') { // (5)
useJUnitPlatform()
}
-
Apply plugins to the build.
-
Define the locations where dependencies can be found.
-
Add dependencies.
-
Set properties.
-
Register and configure tasks.
1. 将插件应用到构建中
插件用于扩展 Gradle。它们还用于模块化和重用项目配置。
可以使用插件脚本块应用插件PluginDependenciesSpec
。
插件块是首选:
plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.0"
id("application")
}
在示例中,application
已应用 Gradle 附带的插件,将我们的项目描述为 Java 应用程序。
Kotlin gradle 插件 1.9.0 版本也已应用。该插件不包含在 Gradle 中,因此必须使用 aplugin id
和 a进行描述plugin version
,以便 Gradle 可以找到并应用它。
2.定义可以找到依赖的位置
一个项目通常具有完成其工作所需的许多依赖项。依赖项包括 Gradle 必须下载才能成功构建的插件、库或组件。
构建脚本让 Gradle 知道在哪里查找依赖项的二进制文件。可以提供多个位置:
repositories {
mavenCentral()
google()
}
在示例中,将从Maven Central Repository下载guava
库和 JetBrains Kotlin 插件 ( ) 。org.jetbrains.kotlin.jvm
3.添加依赖
一个项目通常具有完成其工作所需的许多依赖项。这些依赖项通常是在项目源代码中导入的预编译类的库。
依赖项通过配置进行管理并从存储库中检索。
使用DependencyHandler
byProject.getDependencies()
方法返回的值来管理依赖关系。使用RepositoryHandler
byProject.getRepositories()
方法返回来管理存储库。
dependencies {
implementation("com.google.guava:guava:32.1.1-jre")
}
在示例中,应用程序代码使用 Google 的guava
库。 Guava 提供了用于集合、缓存、原语支持、并发、通用注释、字符串处理、I/O 和验证的实用方法。
4. 设置属性
插件可以使用扩展向项目添加属性和方法。
该Project
对象有一个关联ExtensionContainer
对象,其中包含已应用于项目的插件的所有设置和属性。
在示例中,application
插件添加了一个application
属性,用于详细说明 Java 应用程序的主类:
application {
mainClass = "com.example.Main"
}
5. 注册并配置任务
任务执行一些基本工作,例如编译类、运行单元测试或压缩 WAR 文件。
虽然任务通常在插件中定义,但您可能需要在构建脚本中注册或配置任务。
注册任务会将任务添加到您的项目中。
您可以使用以下方法在项目中注册任务TaskContainer.register(java.lang.String)
:
tasks.register<Zip>("zip-reports") {
from 'Reports/'
include '*'
archiveName 'Reports.zip'
destinationDir(file('/dir'))
}
您可能已经看到了应该避免的TaskContainer.create(java.lang.String)
方法的用法:
tasks.create<Zip>("zip-reports") {
from 'Reports/'
include '*'
archiveName 'Reports.zip'
destinationDir(file('/dir'))
}
提示
|
register() ,它可以避免任务配置,优于create() .
|
您可以使用以下方法找到任务并对其进行配置TaskCollection.named(java.lang.String)
:
tasks.named<Test>("test") {
useJUnitPlatform()
}
下面的示例将Javadoc
任务配置为从 Java 代码自动生成 HTML 文档:
tasks.named("javadoc").configure {
exclude 'app/Internal*.java'
exclude 'app/internal/*'
exclude 'app/internal/*'
}
构建脚本
构建脚本由零个或多个语句和脚本块组成:
println(project.layout.projectDirectory);
语句可以包括方法调用、属性分配和局部变量定义:
version = '1.0.0.GA'
脚本块是一个方法调用,它采用闭包/lambda 作为参数:
configurations {
}
闭包/lambda 在执行时配置一些委托对象:
repositories {
google()
}
构建脚本也是 Groovy 或 Kotlin 脚本:
tasks.register("upper") {
doLast {
val someString = "mY_nAmE"
println("Original: $someString")
println("Upper case: ${someString.toUpperCase()}")
}
}
tasks.register('upper') {
doLast {
String someString = 'mY_nAmE'
println "Original: $someString"
println "Upper case: ${someString.toUpperCase()}"
}
}
$ gradle -q upper Original: mY_nAmE Upper case: MY_NAME
它可以包含 Groovy 或 Kotlin 脚本中允许的元素,例如方法定义和类定义:
tasks.register("count") {
doLast {
repeat(4) { print("$it ") }
}
}
tasks.register('count') {
doLast {
4.times { print "$it " }
}
}
$ gradle -q count 0 1 2 3
灵活的任务注册
使用 Groovy 或 Kotlin 语言的功能,您可以在循环中注册多个任务:
repeat(4) { counter ->
tasks.register("task$counter") {
doLast {
println("I'm task number $counter")
}
}
}
4.times { counter ->
tasks.register("task$counter") {
doLast {
println "I'm task number $counter"
}
}
}
$ gradle -q task1 I'm task number 1
声明变量
构建脚本可以声明两个变量:局部变量和额外属性。
局部变量
使用关键字声明局部变量val
。局部变量仅在声明它们的范围内可见。它们是底层 Kotlin 语言的一个功能。
使用关键字声明局部变量def
。局部变量仅在声明它们的范围内可见。它们是底层 Groovy 语言的一个功能。
val dest = "dest"
tasks.register<Copy>("copy") {
from("source")
into(dest)
}
def dest = 'dest'
tasks.register('copy', Copy) {
from 'source'
into dest
}
额外属性
Gradle 的增强对象(包括项目、任务和源集)可以保存用户定义的属性。
通过所属对象的属性添加、读取和设置额外的属性extra
。或者,您可以使用 .kotlin 委托属性来访问额外的属性by extra
。
通过所属对象的属性添加、读取和设置额外的属性ext
。或者,您可以使用ext
块同时添加多个属性。
plugins {
id("java-library")
}
val springVersion by extra("3.1.0.RELEASE")
val emailNotification by extra { "build@master.org" }
sourceSets.all { extra["purpose"] = null }
sourceSets {
main {
extra["purpose"] = "production"
}
test {
extra["purpose"] = "test"
}
create("plugin") {
extra["purpose"] = "production"
}
}
tasks.register("printProperties") {
val springVersion = springVersion
val emailNotification = emailNotification
val productionSourceSets = provider {
sourceSets.matching { it.extra["purpose"] == "production" }.map { it.name }
}
doLast {
println(springVersion)
println(emailNotification)
productionSourceSets.get().forEach { println(it) }
}
}
plugins {
id 'java-library'
}
ext {
springVersion = "3.1.0.RELEASE"
emailNotification = "build@master.org"
}
sourceSets.all { ext.purpose = null }
sourceSets {
main {
purpose = "production"
}
test {
purpose = "test"
}
plugin {
purpose = "production"
}
}
tasks.register('printProperties') {
def springVersion = springVersion
def emailNotification = emailNotification
def productionSourceSets = provider {
sourceSets.matching { it.purpose == "production" }.collect { it.name }
}
doLast {
println springVersion
println emailNotification
productionSourceSets.get().each { println it }
}
}
$ gradle -q printProperties 3.1.0.RELEASE build@master.org main plugin
project
此示例通过 向对象添加两个额外的属性by extra
。此外,此示例purpose
通过设置extra["purpose"]
为为每个源集添加一个名为 的属性null
。添加后,您可以通过读取和设置这些属性extra
。
project
此示例通过块向对象添加两个额外属性ext
。此外,此示例purpose
通过设置ext.purpose
为为每个源集添加一个名为 的属性null
。添加后,您可以像预定义的属性一样读取和设置所有这些属性。
Gradle 需要特殊的语法来添加属性,以便它可以快速失败。例如,这允许 Gradle 识别脚本何时尝试设置不存在的属性。您可以在任何可以访问其所属对象的地方访问额外的属性。这为额外属性提供了比局部变量更广泛的范围。子项目可以访问其父项目的额外属性。
有关额外属性的更多信息,请参阅 API 文档中的ExtraPropertiesExtension 。
配置任意对象
示例greet()
任务显示了任意对象配置的示例:
class UserInfo(
var name: String? = null,
var email: String? = null
)
tasks.register("configure") {
val user = UserInfo().apply {
name = "Isaac Newton"
email = "isaac@newton.me"
}
doLast {
println(user.name)
println(user.email)
}
}
class UserInfo {
String name
String email
}
tasks.register('configure') {
def user = configure(new UserInfo()) {
name = "Isaac Newton"
email = "isaac@newton.me"
}
doLast {
println user.name
println user.email
}
}
$ gradle -q greet Isaac Newton isaac@newton.me
闭幕代表
每个闭包都有一个delegate
对象。 Groovy 使用此委托来查找对非局部变量和闭包参数的变量和方法引用。 Gradle 将其用于配置闭包,其中delegate
对象指的是正在配置的对象。
dependencies {
assert delegate == project.dependencies
testImplementation('junit:junit:4.13')
delegate.testImplementation('junit:junit:4.13')
}
默认导入
为了使构建脚本更加简洁,Gradle 会自动向脚本添加一组 import 语句。
因此,throw new org.gradle.api.tasks.StopExecutionException()
您可以不写throw new StopExecutionException()
而是写。
Gradle 隐式地将以下导入添加到每个脚本中:
import org.gradle.*
import org.gradle.api.*
import org.gradle.api.artifacts.*
import org.gradle.api.artifacts.component.*
import org.gradle.api.artifacts.dsl.*
import org.gradle.api.artifacts.ivy.*
import org.gradle.api.artifacts.maven.*
import org.gradle.api.artifacts.query.*
import org.gradle.api.artifacts.repositories.*
import org.gradle.api.artifacts.result.*
import org.gradle.api.artifacts.transform.*
import org.gradle.api.artifacts.type.*
import org.gradle.api.artifacts.verification.*
import org.gradle.api.attributes.*
import org.gradle.api.attributes.java.*
import org.gradle.api.attributes.plugin.*
import org.gradle.api.cache.*
import org.gradle.api.capabilities.*
import org.gradle.api.component.*
import org.gradle.api.configuration.*
import org.gradle.api.credentials.*
import org.gradle.api.distribution.*
import org.gradle.api.distribution.plugins.*
import org.gradle.api.execution.*
import org.gradle.api.file.*
import org.gradle.api.flow.*
import org.gradle.api.initialization.*
import org.gradle.api.initialization.definition.*
import org.gradle.api.initialization.dsl.*
import org.gradle.api.initialization.resolve.*
import org.gradle.api.invocation.*
import org.gradle.api.java.archives.*
import org.gradle.api.jvm.*
import org.gradle.api.launcher.cli.*
import org.gradle.api.logging.*
import org.gradle.api.logging.configuration.*
import org.gradle.api.model.*
import org.gradle.api.plugins.*
import org.gradle.api.plugins.antlr.*
import org.gradle.api.plugins.catalog.*
import org.gradle.api.plugins.jvm.*
import org.gradle.api.plugins.quality.*
import org.gradle.api.plugins.scala.*
import org.gradle.api.problems.*
import org.gradle.api.provider.*
import org.gradle.api.publish.*
import org.gradle.api.publish.ivy.*
import org.gradle.api.publish.ivy.plugins.*
import org.gradle.api.publish.ivy.tasks.*
import org.gradle.api.publish.maven.*
import org.gradle.api.publish.maven.plugins.*
import org.gradle.api.publish.maven.tasks.*
import org.gradle.api.publish.plugins.*
import org.gradle.api.publish.tasks.*
import org.gradle.api.reflect.*
import org.gradle.api.reporting.*
import org.gradle.api.reporting.components.*
import org.gradle.api.reporting.dependencies.*
import org.gradle.api.reporting.dependents.*
import org.gradle.api.reporting.model.*
import org.gradle.api.reporting.plugins.*
import org.gradle.api.resources.*
import org.gradle.api.services.*
import org.gradle.api.specs.*
import org.gradle.api.tasks.*
import org.gradle.api.tasks.ant.*
import org.gradle.api.tasks.application.*
import org.gradle.api.tasks.bundling.*
import org.gradle.api.tasks.compile.*
import org.gradle.api.tasks.diagnostics.*
import org.gradle.api.tasks.diagnostics.configurations.*
import org.gradle.api.tasks.incremental.*
import org.gradle.api.tasks.javadoc.*
import org.gradle.api.tasks.options.*
import org.gradle.api.tasks.scala.*
import org.gradle.api.tasks.testing.*
import org.gradle.api.tasks.testing.junit.*
import org.gradle.api.tasks.testing.junitplatform.*
import org.gradle.api.tasks.testing.testng.*
import org.gradle.api.tasks.util.*
import org.gradle.api.tasks.wrapper.*
import org.gradle.api.toolchain.management.*
import org.gradle.authentication.*
import org.gradle.authentication.aws.*
import org.gradle.authentication.http.*
import org.gradle.build.event.*
import org.gradle.buildinit.*
import org.gradle.buildinit.plugins.*
import org.gradle.buildinit.tasks.*
import org.gradle.caching.*
import org.gradle.caching.configuration.*
import org.gradle.caching.http.*
import org.gradle.caching.local.*
import org.gradle.concurrent.*
import org.gradle.external.javadoc.*
import org.gradle.ide.visualstudio.*
import org.gradle.ide.visualstudio.plugins.*
import org.gradle.ide.visualstudio.tasks.*
import org.gradle.ide.xcode.*
import org.gradle.ide.xcode.plugins.*
import org.gradle.ide.xcode.tasks.*
import org.gradle.ivy.*
import org.gradle.jvm.*
import org.gradle.jvm.application.scripts.*
import org.gradle.jvm.application.tasks.*
import org.gradle.jvm.tasks.*
import org.gradle.jvm.toolchain.*
import org.gradle.language.*
import org.gradle.language.assembler.*
import org.gradle.language.assembler.plugins.*
import org.gradle.language.assembler.tasks.*
import org.gradle.language.base.*
import org.gradle.language.base.artifact.*
import org.gradle.language.base.compile.*
import org.gradle.language.base.plugins.*
import org.gradle.language.base.sources.*
import org.gradle.language.c.*
import org.gradle.language.c.plugins.*
import org.gradle.language.c.tasks.*
import org.gradle.language.cpp.*
import org.gradle.language.cpp.plugins.*
import org.gradle.language.cpp.tasks.*
import org.gradle.language.java.artifact.*
import org.gradle.language.jvm.tasks.*
import org.gradle.language.nativeplatform.*
import org.gradle.language.nativeplatform.tasks.*
import org.gradle.language.objectivec.*
import org.gradle.language.objectivec.plugins.*
import org.gradle.language.objectivec.tasks.*
import org.gradle.language.objectivecpp.*
import org.gradle.language.objectivecpp.plugins.*
import org.gradle.language.objectivecpp.tasks.*
import org.gradle.language.plugins.*
import org.gradle.language.rc.*
import org.gradle.language.rc.plugins.*
import org.gradle.language.rc.tasks.*
import org.gradle.language.scala.tasks.*
import org.gradle.language.swift.*
import org.gradle.language.swift.plugins.*
import org.gradle.language.swift.tasks.*
import org.gradle.maven.*
import org.gradle.model.*
import org.gradle.nativeplatform.*
import org.gradle.nativeplatform.platform.*
import org.gradle.nativeplatform.plugins.*
import org.gradle.nativeplatform.tasks.*
import org.gradle.nativeplatform.test.*
import org.gradle.nativeplatform.test.cpp.*
import org.gradle.nativeplatform.test.cpp.plugins.*
import org.gradle.nativeplatform.test.cunit.*
import org.gradle.nativeplatform.test.cunit.plugins.*
import org.gradle.nativeplatform.test.cunit.tasks.*
import org.gradle.nativeplatform.test.googletest.*
import org.gradle.nativeplatform.test.googletest.plugins.*
import org.gradle.nativeplatform.test.plugins.*
import org.gradle.nativeplatform.test.tasks.*
import org.gradle.nativeplatform.test.xctest.*
import org.gradle.nativeplatform.test.xctest.plugins.*
import org.gradle.nativeplatform.test.xctest.tasks.*
import org.gradle.nativeplatform.toolchain.*
import org.gradle.nativeplatform.toolchain.plugins.*
import org.gradle.normalization.*
import org.gradle.platform.*
import org.gradle.platform.base.*
import org.gradle.platform.base.binary.*
import org.gradle.platform.base.component.*
import org.gradle.platform.base.plugins.*
import org.gradle.plugin.devel.*
import org.gradle.plugin.devel.plugins.*
import org.gradle.plugin.devel.tasks.*
import org.gradle.plugin.management.*
import org.gradle.plugin.use.*
import org.gradle.plugins.ear.*
import org.gradle.plugins.ear.descriptor.*
import org.gradle.plugins.ide.*
import org.gradle.plugins.ide.api.*
import org.gradle.plugins.ide.eclipse.*
import org.gradle.plugins.ide.idea.*
import org.gradle.plugins.signing.*
import org.gradle.plugins.signing.signatory.*
import org.gradle.plugins.signing.signatory.pgp.*
import org.gradle.plugins.signing.type.*
import org.gradle.plugins.signing.type.pgp.*
import org.gradle.process.*
import org.gradle.swiftpm.*
import org.gradle.swiftpm.plugins.*
import org.gradle.swiftpm.tasks.*
import org.gradle.testing.base.*
import org.gradle.testing.base.plugins.*
import org.gradle.testing.jacoco.plugins.*
import org.gradle.testing.jacoco.tasks.*
import org.gradle.testing.jacoco.tasks.rules.*
import org.gradle.testkit.runner.*
import org.gradle.util.*
import org.gradle.vcs.*
import org.gradle.vcs.git.*
import org.gradle.work.*
import org.gradle.workers.*
下一步: 了解如何使用任务>>
使用任务
Gradle 可以在项目上执行的工作由一项或多项任务定义。
任务代表构建执行的某些独立工作单元。这可能是编译一些类、创建 JAR、生成 Javadoc 或将一些档案发布到存储库。
当用户./gradlew build
在命令行中运行时,Gradle 将执行该build
任务及其依赖的任何其他任务。
列出可用任务
Gradle 为项目提供了几个默认任务,通过运行列出./gradlew tasks
:
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project 'myTutorial'
------------------------------------------------------------
Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'myTutorial'.
...
任务来自构建脚本或插件。
一旦我们将插件应用到我们的项目中,例如application
插件,其他任务就变得可用:
plugins {
id("application")
}
$ ./gradlew tasks
> Task :tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Application tasks
-----------------
run - Runs this project as a JVM application
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Other tasks
-----------
compileJava - Compiles main Java source.
...
开发人员应该熟悉其中的许多任务,例如assemble
、build
、 和。run
任务分类
可以执行两类任务:
-
可操作的任务附加了一些操作来在您的构建中完成工作:
compileJava
。 -
生命周期任务是没有附加操作的任务:
assemble
,build
。
通常,生命周期任务取决于许多可操作的任务,并且用于一次执行许多任务。
任务注册和行动
让我们看一下构建脚本中的一个简单的“Hello World”任务:
tasks.register("hello") {
doLast {
println("Hello world!")
}
}
tasks.register('hello') {
doLast {
println 'Hello world!'
}
}
在示例中,构建脚本注册hello
一个使用TaskContainer API调用的单个任务,并向其添加一个操作。
如果列出了项目中的任务,则该hello
任务可供 Gradle 使用:
$ ./gradlew app:tasks --all
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Other tasks
-----------
compileJava - Compiles main Java source.
compileTestJava - Compiles test Java source.
hello
processResources - Processes main resources.
processTestResources - Processes test resources.
startScripts - Creates OS-specific scripts to run the project as a JVM application.
您可以使用以下命令在构建脚本中执行任务./gradlew hello
:
$ ./gradlew hello Hello world!
当 Gradle 执行hello
任务时,它会执行提供的操作。在这种情况下,操作只是一个包含一些代码的块:println("Hello world!")
。
任务组和描述
hello
上一节中的任务可以通过描述进行详细说明,并通过以下更新分配给一个组:
tasks.register("hello") {
group = "Custom"
description = "A lovely greeting task."
doLast {
println("Hello world!")
}
}
将任务分配给组后,它将按以下方式列出./gradlew tasks
:
$ ./gradlew tasks
> Task :tasks
Custom tasks
------------------
hello - A lovely greeting task.
要查看有关任务的信息,请使用以下help --task <task-name>
命令:
$./gradlew help --task hello
> Task :help
Detailed task information for hello
Path
:app:hello
Type
Task (org.gradle.api.Task)
Options
--rerun Causes the task to be re-run even if up-to-date.
Description
A lovely greeting task.
Group
Custom
正如我们所看到的,该hello
任务属于该custom
组。
任务依赖关系
您可以声明依赖于其他任务的任务:
tasks.register("hello") {
doLast {
println("Hello world!")
}
}
tasks.register("intro") {
dependsOn("hello")
doLast {
println("I'm Gradle")
}
}
tasks.register('hello') {
doLast {
println 'Hello world!'
}
}
tasks.register('intro') {
dependsOn tasks.hello
doLast {
println "I'm Gradle"
}
}
$ gradle -q intro Hello world! I'm Gradle
taskX
to的依赖taskY
可以在taskY
定义之前声明:
tasks.register("taskX") {
dependsOn("taskY")
doLast {
println("taskX")
}
}
tasks.register("taskY") {
doLast {
println("taskY")
}
}
tasks.register('taskX') {
dependsOn 'taskY'
doLast {
println 'taskX'
}
}
tasks.register('taskY') {
doLast {
println 'taskY'
}
}
$ gradle -q taskX taskY taskX
hello
上一个示例中的任务已更新以包含依赖项:
tasks.register("hello") {
group = "Custom"
description = "A lovely greeting task."
doLast {
println("Hello world!")
}
dependsOn(tasks.assemble)
}
现在任务hello
依赖于assemble
任务,这意味着 Gradle 必须先assemble
执行任务才能执行任务:hello
$ ./gradlew :app:hello
> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:startScripts UP-TO-DATE
> Task :app:distTar UP-TO-DATE
> Task :app:distZip UP-TO-DATE
> Task :app:assemble UP-TO-DATE
> Task :app:hello
Hello world!
任务配置
注册后,可以通过TaskProvider API访问任务以进行进一步配置。
例如,您可以使用它在运行时动态向任务添加依赖项:
repeat(4) { counter ->
tasks.register("task$counter") {
doLast {
println("I'm task number $counter")
}
}
}
tasks.named("task0") { dependsOn("task2", "task3") }
4.times { counter ->
tasks.register("task$counter") {
doLast {
println "I'm task number $counter"
}
}
}
tasks.named('task0') { dependsOn('task2', 'task3') }
$ gradle -q task0 I'm task number 2 I'm task number 3 I'm task number 0
或者您可以向现有任务添加行为:
tasks.register("hello") {
doLast {
println("Hello Earth")
}
}
tasks.named("hello") {
doFirst {
println("Hello Venus")
}
}
tasks.named("hello") {
doLast {
println("Hello Mars")
}
}
tasks.named("hello") {
doLast {
println("Hello Jupiter")
}
}
tasks.register('hello') {
doLast {
println 'Hello Earth'
}
}
tasks.named('hello') {
doFirst {
println 'Hello Venus'
}
}
tasks.named('hello') {
doLast {
println 'Hello Mars'
}
}
tasks.named('hello') {
doLast {
println 'Hello Jupiter'
}
}
$ gradle -q hello Hello Venus Hello Earth Hello Mars Hello Jupiter
提示
|
调用doFirst 和doLast 可以多次执行。他们将操作添加到任务操作列表的开头或结尾。任务执行时,会按顺序执行动作列表中的动作。
|
named
以下是用于配置插件添加的任务的方法示例:
tasks.named("dokkaHtml") {
outputDirectory.set(buildDir.resolve("dokka"))
}
任务类型
Gradle 任务是Task
.
在构建脚本中,HelloTask
该类是通过扩展创建的DefaultTask
:
// Extend the DefaultTask class to create a HelloTask class
abstract class HelloTask : DefaultTask() {
@TaskAction
fun hello() {
println("hello from HelloTask")
}
}
// Register the hello Task with type HelloTask
tasks.register<HelloTask>("hello") {
group = "Custom tasks"
description = "A lovely greeting task."
}
该hello
任务已注册为type HelloTask
。
执行我们的新hello
任务:
$ ./gradlew hello
> Task :app:hello
hello from HelloTask
现在hello
任务是 typeHelloTask
而不是 type Task
。
Gradlehelp
任务揭示了变化:
$ ./gradlew help --task hello
> Task :help
Detailed task information for hello
Path
:app:hello
Type
HelloTask (Build_gradle$HelloTask)
Options
--rerun Causes the task to be re-run even if up-to-date.
Description
A lovely greeting task.
Group
Custom tasks
写作任务
Gradle 任务是通过扩展DefaultTask
.
但是,该泛型DefaultTask
没有为 Gradle 提供任何操作。如果用户想要扩展 Gradle 及其构建脚本的功能,他们必须使用内置任务或创建自定义任务:
-
内置任务- Gradle 提供内置实用任务,例如
Copy
、Jar
、Zip
、Delete
等... -
自定义任务- Gradle 允许用户子类化
DefaultTask
以创建自己的任务类型。
创建任务
创建自定义任务最简单、最快捷的方法是在构建脚本中:
要创建任务,请从该类继承DefaultTask
并实现@TaskAction
处理程序:
abstract class CreateFileTask : DefaultTask() {
@TaskAction
fun action() {
val file = File("myfile.txt")
file.createNewFile()
file.writeText("HELLO FROM MY TASK")
}
}
它CreateFileTask
实现了一组简单的操作。首先,在主项目中创建一个名为“myfile.txt”的文件。然后,一些文本被写入该文件。
注册任务
使用该方法在构建脚本中注册任务TaskContainer.register()
,然后可以在构建逻辑中使用它。
abstract class CreateFileTask : DefaultTask() {
@TaskAction
fun action() {
val file = File("myfile.txt")
file.createNewFile()
file.writeText("HELLO FROM MY TASK")
}
}
tasks.register<CreateFileTask>("createFileTask")
任务组和描述
设置任务的组和描述属性可以帮助用户了解如何使用您的任务:
abstract class CreateFileTask : DefaultTask() {
@TaskAction
fun action() {
val file = File("myfile.txt")
file.createNewFile()
file.writeText("HELLO FROM MY TASK")
}
}
tasks.register<CreateFileTask>("createFileTask", ) {
group = "custom"
description = "Create myfile.txt in the current directory"
}
将任务添加到组后,在列出任务时即可看到该任务。
任务输入和输出
为了让任务做有用的工作,它通常需要一些输入。任务通常会产生输出。
abstract class CreateFileTask : DefaultTask() {
@Input
val fileText = "HELLO FROM MY TASK"
@Input
val fileName = "myfile.txt"
@OutputFile
val myFile: File = File(fileName)
@TaskAction
fun action() {
myFile.createNewFile()
myFile.writeText(fileText)
}
}
tasks.register<CreateFileTask>("createFileTask") {
group = "custom"
description = "Create myfile.txt in the current directory"
}
配置任务
使用该方法可以选择在构建脚本中配置任务TaskCollection.named()
。
该类CreateFileTask
已更新,以便文件中的文本是可配置的:
abstract class CreateFileTask : DefaultTask() {
@get:Input
abstract val fileText: Property<String>
@Input
val fileName = "myfile.txt"
@OutputFile
val myFile: File = File(fileName)
@TaskAction
fun action() {
myFile.createNewFile()
myFile.writeText(fileText.get())
}
}
tasks.register<CreateFileTask>("createFileTask") {
group = "custom"
description = "Create myfile.txt in the current directory"
fileText.convention("HELLO FROM THE CREATE FILE TASK METHOD") // Set convention
}
tasks.named<CreateFileTask>("createFileTask") {
fileText.set("HELLO FROM THE NAMED METHOD") // Override with custom message
}
在该named()
方法中,我们找到createFileTask
任务并设置将写入文件的文本。
任务执行时:
$ ./gradlew createFileTask
> Configure project :app
> Task :app:createFileTask
BUILD SUCCESSFUL in 5s
2 actionable tasks: 1 executed, 1 up-to-date
myfile.txt
在项目根文件夹中创建一个名为的文本文件:
HELLO FROM THE NAMED METHOD
请参阅“开发 Gradle 任务”一章以了解更多信息。
下一步: 了解如何使用插件>>
使用插件
Gradle 的大部分功能都是通过插件提供的,包括随 Gradle 分发的核心插件、第三方插件以及构建中定义的脚本插件。
插件引入新任务(例如,JavaCompile
)、域对象(例如,SourceSet
)、约定(例如,将 Java 源定位于src/main/java
),并扩展核心或其他插件对象。
Gradle 中的插件对于自动化常见构建任务、与外部工具或服务集成以及定制构建过程以满足特定项目需求至关重要。它们还充当组织构建逻辑的主要机制。
插件的好处
在构建脚本中编写许多任务并复制配置块可能会变得混乱。与直接向构建脚本添加逻辑相比,插件具有以下几个优点:
-
促进可重用性:减少跨项目重复类似逻辑的需要。
-
增强模块化:允许更加模块化和有组织的构建脚本。
-
封装逻辑:保持命令式逻辑分离,从而实现更具声明性的构建脚本。
插件分发
您可以利用 Gradle 和 Gradle 社区的插件,也可以创建自己的插件。
插件可以通过三种方式使用:
-
核心插件- Gradle 开发并维护一组核心插件。
-
社区插件- 在远程存储库(例如 Maven 或Gradle 插件门户)中共享的 Gradle 插件。
-
本地插件- Gradle 使用户能够使用API创建自定义插件。
插件类型
插件可以实现为二进制插件、预编译脚本插件或脚本插件:
- 二进制插件
-
二进制插件是通常用 Java 或 Kotlin DSL 编写的编译插件,打包为 JAR 文件。它们被应用到使用块的项目中
plugins {}
。与脚本插件或预编译脚本插件相比,它们提供更好的性能和可维护性。 - 预编译脚本插件
-
预编译脚本插件是 Groovy DSL 或 Kotlin DSL 脚本,它们被编译并作为打包在库中的 Java 类文件分发。它们被应用到使用块的项目中
plugins {}
。它们提供了一种跨项目重用复杂逻辑的方法,并允许更好地组织构建逻辑。 - 脚本插件
-
脚本插件是 Groovy DSL 或 Kotlin DSL 脚本,它们使用语法直接应用于 Gradle 构建脚本
apply from:
。它们在构建脚本中内联应用,以添加功能或自定义构建过程。它们使用起来很简单。
插件通常作为脚本插件启动(因为它们很容易编写)。然后,随着代码变得更有价值,它会被迁移到可以在多个项目或组织之间轻松测试和共享的二进制插件。
使用插件
要使用插件中封装的构建逻辑,Gradle 需要执行两个步骤。首先,它需要解析插件,然后需要将插件应用到目标,通常是Project
.
-
解析插件意味着找到包含给定插件的 JAR 的正确版本并将其添加到脚本类路径中。一旦插件被解析,它的 API 就可以在构建脚本中使用。脚本插件是自我解析的,因为它们是从应用它们时提供的特定文件路径或 URL 解析的。作为 Gradle 发行版的一部分提供的核心二进制插件会自动解析。
-
应用插件意味着在项目上执行插件的Plugin.apply(T) 。
建议使用插件DSL一步解决并应用插件。
解决插件问题
Gradle 提供了核心插件(例如,JavaPlugin
、GroovyPlugin
、MavenPublishPlugin
等)作为其分发的一部分,这意味着它们会自动解析。
核心插件使用插件名称应用在构建脚本中:
plugins {
id «plugin name»
}
例如:
plugins {
id("java")
}
非核心插件必须先解析后才能应用。非核心插件由构建文件中的唯一 ID 和版本来标识:
plugins {
id «plugin id» version «plugin version»
}
并且必须在设置文件中指定插件的位置:
pluginManagement {
repositories {
gradlePluginPortal()
}
maven {
url 'https://maven.example.com/plugins'
}
}
解决和应用插件还有其他注意事项:
# | 到 | 使用 | 例如: |
---|---|---|---|
将核心、社区或本地插件应用到特定项目。 |
|
||
将通用核心、社区或本地插件应用到多个子项目。 |
|
||
应用构建脚本本身所需的核心、社区或本地插件。 |
|
||
应用本地脚本插件。 |
|
plugins{}
1. 使用块应用插件
插件 DSL 提供了一种简洁便捷的方式来声明插件依赖项。
插件块配置一个实例PluginDependenciesSpec
:
plugins {
application // by name
java // by name
id("java") // by id - recommended
id("org.jetbrains.kotlin.jvm") version "1.9.0" // by id - recommended
}
核心 Gradle 插件的独特之处在于它们提供短名称,例如java
核心JavaPlugin。
要应用核心插件,可以使用短名称:
plugins {
java
}
plugins {
id 'java'
}
所有其他二进制插件必须使用插件 ID 的完全限定形式(例如com.github.foo.bar
)。
要从Gradle 插件门户应用社区插件,必须使用完全限定的插件 id (全局唯一标识符):
plugins {
id("com.jfrog.bintray") version "1.8.5"
}
plugins {
id 'com.jfrog.bintray' version '1.8.5'
}
PluginDependenciesSpec
有关使用插件 DSL 的更多信息,请参阅 参考资料。
插件 DSL 的限制
插件 DSL 为用户提供了方便的语法,并且让 Gradle 能够快速确定使用哪些插件。这使得 Gradle 能够:
-
优化插件类的加载和复用。
-
为编辑者提供有关构建脚本中潜在属性和值的详细信息。
然而,DSL 要求插件是静态定义的。
plugins {}
区块机制和“传统”方法机制之间存在一些关键的区别apply()
。还有一些限制和可能的限制。
约束语法
该plugins {}
块不支持任意代码。
它被限制为幂等(每次产生相同的结果)和无副作用(Gradle 可以随时安全执行)。
形式为:
plugins {
id(«plugin id») // (1)
id(«plugin id») version «plugin version» // (2)
}
-
for core Gradle plugins or plugins already available to the build script
-
for binary Gradle plugins that need to be resolved
plugins {
id «plugin id» // (1)
id «plugin id» version «plugin version» // (2)
}
-
for core Gradle plugins or plugins already available to the build script
-
for binary Gradle plugins that need to be resolved
其中«plugin id»
和«plugin version»
是一个字符串。
其中«plugin id»
和«plugin version»
必须是常量、文字字符串。
该plugins{}
块还必须是构建脚本中的顶级语句。它不能嵌套在另一个构造中(例如,if 语句或for 循环)。
仅在构建脚本和设置文件中
该块只能在项目的构建脚本和文件plugins{}
中使用。它必须出现在任何其他块之前。它不能在脚本插件或初始化脚本中使用。build.gradle(.kts)
settings.gradle(.kts)
将插件应用到所有子项目
假设您有一个多项目构建,您可能希望将插件应用于构建中的部分或全部子项目,但不应用于项目root
。
虽然该块的默认行为plugins{}
是立即resolve
使用 apply
插件,但您可以使用apply false
语法告诉 Gradle 不要将插件应用到当前项目。然后,plugins{}
在子项目的构建脚本中使用没有版本的块:
include("hello-a")
include("hello-b")
include("goodbye-c")
plugins {
id("com.example.hello") version "1.0.0" apply false
id("com.example.goodbye") version "1.0.0" apply false
}
plugins {
id("com.example.hello")
}
plugins {
id("com.example.hello")
}
plugins {
id("com.example.goodbye")
}
include 'hello-a'
include 'hello-b'
include 'goodbye-c'
plugins {
id 'com.example.hello' version '1.0.0' apply false
id 'com.example.goodbye' version '1.0.0' apply false
}
plugins {
id 'com.example.hello'
}
plugins {
id 'com.example.hello'
}
plugins {
id 'com.example.goodbye'
}
您还可以通过使用自己的约定插件编写构建逻辑来封装外部插件的版本。
buildSrc
2. 从目录应用插件
buildSrc
是 Gradle 项目根目录中的一个可选目录,其中包含用于构建主项目的构建逻辑(即插件)。您可以应用驻留在项目buildSrc
目录中的插件,只要它们具有已定义的 ID。
以下示例显示如何将my.MyPlugin
在 中定义的插件实现类绑定buildSrc
到 id“my-plugin”:
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("myPlugins") {
id = "my-plugin"
implementationClass = "my.MyPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
myPlugins {
id = 'my-plugin'
implementationClass = 'my.MyPlugin'
}
}
}
然后可以通过 ID 应用该插件:
plugins {
id("my-plugin")
}
plugins {
id 'my-plugin'
}
buildscript{}
3. 使用块应用插件
该buildscript
块用于:
-
全局
dependencies
且repositories
构建项目所需(在子项目中应用)。 -
声明哪些插件可在构建脚本中使用(在
build.gradle(.kts)
文件本身中)。
因此,当您想在构建脚本本身中使用库时,必须使用以下命令将此库添加到脚本类路径上buildScript
:
import org.apache.commons.codec.binary.Base64
buildscript {
repositories { // this is where the plugins are located
mavenCentral()
google()
}
dependencies { // these are the plugins that can be used in subprojects or in the build file itself
classpath group: 'commons-codec', name: 'commons-codec', version: '1.2' // used in the task below
classpath 'com.android.tools.build:gradle:4.1.0' // used in subproject
}
}
tasks.register('encode') {
doLast {
def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
println new String(encodedString)
}
}
您可以在需要它的子项目中应用全局声明的依赖项:
plugins {
id 'com.android.application'
}
作为外部 jar 文件发布的二进制插件可以通过将插件添加到构建脚本类路径然后应用该插件来添加到项目中。
可以使用块将外部 jar 添加到构建脚本类路径,如构建脚本的外部依赖项buildscript{}
中所述:
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5")
}
}
apply(plugin = "com.jfrog.bintray")
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
}
}
apply plugin: 'com.jfrog.bintray'
4. 使用遗留apply()
方法应用脚本插件
脚本插件是一个临时插件,通常在同一构建脚本中编写和应用。它是使用遗留应用程序方法应用的:
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
println("Plugin ${this.javaClass.simpleName} applied on ${project.name}")
}
}
apply<MyPlugin>()
other.gradle
让我们看一个在与该文件位于同一目录中的文件中编写的插件的基本示例build.gradle
:
public class Other implements Plugin<Project> {
@Override
void apply(Project project) {
// Does something
}
}
首先,使用以下命令导入外部文件:
apply from: 'other.gradle'
然后你就可以应用它:
apply plugin: Other
脚本插件会自动解析,并且可以从本地文件系统上的脚本或远程应用:
apply(from = "other.gradle.kts")
apply from: 'other.gradle'
文件系统位置是相对于项目目录的,而远程脚本位置是使用 HTTP URL 指定的。多个脚本插件(任一形式)可以应用于给定目标。
插件管理
该pluginManagement{}
块用于配置插件解析的存储库,并为构建脚本中应用的插件定义版本约束。
该pluginManagement{}
块可以在文件中使用settings.gradle(.kts)
,它必须是文件中的第一个块:
pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
rootProject.name = "plugin-management"
pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
rootProject.name = 'plugin-management'
该块也可以在初始化脚本中使用:
settingsEvaluated {
pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
}
settingsEvaluated { settings ->
settings.pluginManagement {
plugins {
}
resolutionStrategy {
}
repositories {
}
}
}
自定义插件存储库
默认情况下,plugins{}
DSL 解析来自公共Gradle 插件门户的插件。
许多构建作者还希望解析来自私有 Maven 或 Ivy 存储库的插件,因为它们包含专有的实现细节,或者可以更好地控制哪些插件可用于其构建。
要指定自定义插件存储库,请使用repositories{}
内部块pluginManagement{}
:
pluginManagement {
repositories {
maven(url = "./maven-repo")
gradlePluginPortal()
ivy(url = "./ivy-repo")
}
}
pluginManagement {
repositories {
maven {
url './maven-repo'
}
gradlePluginPortal()
ivy {
url './ivy-repo'
}
}
}
这告诉 Gradle 在解析插件时首先查看 Maven 存储库,../maven-repo
如果在 Maven 存储库中找不到插件,则检查 Gradle 插件门户。如果您不想搜索 Gradle 插件门户,请省略该gradlePluginPortal()
行。最后,../ivy-repo
将检查Ivy 存储库。
插件版本管理
plugins{}
内部的块允许pluginManagement{}
在单个位置定义构建的所有插件版本。然后可以通过块通过 id 将插件应用到任何构建脚本plugins{}
。
以这种方式设置插件版本的好处之一是,它pluginManagement.plugins{}
不具有与构建脚本块相同的约束语法plugins{}
。这允许从 获取插件版本gradle.properties
,或通过其他机制加载插件版本。
通过以下方式管理插件版本pluginManagement
:
pluginManagement {
val helloPluginVersion: String by settings
plugins {
id("com.example.hello") version "${helloPluginVersion}"
}
}
plugins {
id("com.example.hello")
}
helloPluginVersion=1.0.0
pluginManagement {
plugins {
id 'com.example.hello' version "${helloPluginVersion}"
}
}
plugins {
id 'com.example.hello'
}
helloPluginVersion=1.0.0
插件版本从设置脚本加载gradle.properties
并在设置脚本中配置,允许将插件添加到任何项目而无需指定版本。
插件解析规则
插件解析规则允许您修改plugins{}
块中发出的插件请求,例如,更改请求的版本或显式指定实现工件坐标。
要添加解析规则,请使用块resolutionStrategy{}
内部pluginManagement{}
:
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == "com.example") {
useModule("com.example:sample-plugins:1.0.0")
}
}
}
repositories {
maven {
url = uri("./maven-repo")
}
gradlePluginPortal()
ivy {
url = uri("./ivy-repo")
}
}
}
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == 'com.example') {
useModule('com.example:sample-plugins:1.0.0')
}
}
}
repositories {
maven {
url './maven-repo'
}
gradlePluginPortal()
ivy {
url './ivy-repo'
}
}
}
这告诉 Gradle 使用指定的插件实现工件,而不是其从插件 ID 到 Maven/Ivy 坐标的内置默认映射。
自定义 Maven 和 Ivy 插件存储库必须包含插件标记工件和实现该插件的工件。请阅读Gradle 插件开发插件,了解有关将插件发布到自定义存储库的更多信息。
有关使用该块的完整文档,请参阅PluginManagementSpecpluginManagement{}
。
插件标记工件
由于plugins{}
DSL 块仅允许通过全局唯一的插件id
和version
属性来声明插件,因此 Gradle 需要一种方法来查找插件实现工件的坐标。
为此,Gradle 将查找坐标为 的插件标记工件plugin.id:plugin.id.gradle.plugin:plugin.version
。该标记需要依赖于实际的插件实现。发布这些标记是由java-gradle-plugin自动完成的。
例如,项目中的以下完整示例sample-plugins
展示了如何使用java-gradle-plugin、maven-publish插件和ivy-publish插件的组合将插件发布到 Ivy 和 Maven 存储com.example.hello
库。com.example.goodbye
plugins {
`java-gradle-plugin`
`maven-publish`
`ivy-publish`
}
group = "com.example"
version = "1.0.0"
gradlePlugin {
plugins {
create("hello") {
id = "com.example.hello"
implementationClass = "com.example.hello.HelloPlugin"
}
create("goodbye") {
id = "com.example.goodbye"
implementationClass = "com.example.goodbye.GoodbyePlugin"
}
}
}
publishing {
repositories {
maven {
url = uri(layout.buildDirectory.dir("maven-repo"))
}
ivy {
url = uri(layout.buildDirectory.dir("ivy-repo"))
}
}
}
plugins {
id 'java-gradle-plugin'
id 'maven-publish'
id 'ivy-publish'
}
group 'com.example'
version '1.0.0'
gradlePlugin {
plugins {
hello {
id = 'com.example.hello'
implementationClass = 'com.example.hello.HelloPlugin'
}
goodbye {
id = 'com.example.goodbye'
implementationClass = 'com.example.goodbye.GoodbyePlugin'
}
}
}
publishing {
repositories {
maven {
url layout.buildDirectory.dir("maven-repo")
}
ivy {
url layout.buildDirectory.dir("ivy-repo")
}
}
}
在示例目录中运行gradle publish
会创建以下 Maven 存储库布局(Ivy 布局类似):
旧版插件应用程序
随着插件 DSL的引入,用户应该没有理由使用应用插件的传统方法。此处记录是为了防止构建作者由于当前工作方式的限制而无法使用插件 DSL。
apply(plugin = "java")
apply plugin: 'java'
可以使用插件 id应用插件。在上面的例子中,我们使用短名称“java”来应用JavaPlugin。
除了使用插件 ID 之外,还可以通过简单地指定插件的类来应用插件:
apply<JavaPlugin>()
apply plugin: JavaPlugin
JavaPlugin
上面示例中的符号指的是JavaPlugin。此类并不严格需要导入,因为该org.gradle.api.plugins
包会在所有构建脚本中自动导入(请参阅默认导入)。
此外,在 Kotlin 中而不是在 Java 中,需要附加::class
后缀来标识类文字.class
。
此外,在 Groovy 中不需要.class
像在 Java 中那样通过追加来标识类文字。
使用版本目录
当项目使用版本目录时,应用时可以通过别名引用插件。
我们来看一个简单的版本目录:
[versions]
intellij-plugin = "1.6"
[plugins]
jetbrains-intellij = { id = "org.jetbrains.intellij", version.ref = "intellij-plugin" }
然后可以使用以下方法将插件应用于任何构建脚本alias
:
plugins {
alias(libs.plugins.jetbrains.intellij)
}
提示
|
jetbrains-intellij 可用作 Gradle 生成的安全访问器:jetbrains.intellij .
|
下一步: 学习如何编写插件>>
编写插件
如果 Gradle 或 Gradle 社区没有提供您的项目所需的特定功能,创建您自己的插件可能是一个解决方案。
此外,如果您发现自己在子项目中重复构建逻辑,并且需要更好的方式来组织它,自定义插件可以提供帮助。
自定义插件
插件是实现该Plugin
接口的任何类。下面的例子是最简单的插件,一个“hello world”插件:
import org.gradle.api.Plugin
import org.gradle.api.Project
abstract class SamplePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.create("SampleTask") {
println("Hello world!")
}
}
}
脚本插件
许多插件都是以构建脚本中编码的脚本插件开始的。这提供了一种在构建插件时快速原型化和实验的简单方法。让我们看一个例子:
// Define a task
abstract class CreateFileTask : DefaultTask() { // (1)
@get:Input
abstract val fileText: Property<String> // (2)
@Input
val fileName = "myfile.txt"
@OutputFile
val myFile: File = File(fileName)
@TaskAction
fun action() {
myFile.createNewFile()
myFile.writeText(fileText.get())
}
}
// Define a plugin
abstract class MyPlugin : Plugin<Project> { // (3)
override fun apply(project: Project) {
tasks {
register("createFileTask", CreateFileTask::class) {
group = "from my plugin"
description = "Create myfile.txt in the current directory"
fileText.set("HELLO FROM MY PLUGIN")
}
}
}
}
// Apply the local plugin
apply<MyPlugin>() // (4)
-
子类
DefaultTask()
. -
在任务中使用惰性配置。
-
扩展
org.gradle.api.Plugin
接口。 -
应用脚本插件。
1. 子类DefaultTask()
首先,通过子类化来构建任务DefaultTask()
。
abstract class CreateFileTask : DefaultTask() { }
这个简单的任务将一个文件添加到我们应用程序的根目录中。
2.使用惰性配置
Gradle 有一个称为惰性配置的概念,它允许任务输入和输出在实际设置之前被引用。这是通过Property
类类型完成的。
abstract val fileText: Property<String>
这种机制的一个优点是,您可以将一个任务的输出文件链接到另一个任务的输入文件,所有这些都在文件名确定之前进行。该类Property
还知道它链接到哪个任务,从而使 Gradle 能够自动添加所需的任务依赖项。
3.扩展org.gradle.api.Plugin
接口
接下来,创建一个扩展该接口的新类org.gradle.api.Plugin
。
abstract class MyPlugin : Plugin<Project> {
override fun apply() {}
}
您可以在apply()
方法中添加任务和其他逻辑。
4.应用脚本插件
最后,在构建脚本中应用本地插件。
apply<MyPlugin>()
当MyPlugin
在构建脚本中应用时,Gradle 会调用fun apply() {}
自定义类中定义的方法MyPlugin
。
这使得该插件可供应用程序使用。
笔记
|
不推荐使用脚本插件。脚本插件提供了一种快速原型构建逻辑的简单方法,然后将其迁移到更永久的解决方案(例如约定插件或二进制插件)。 |
约定插件
约定插件是一种在 Gradle 中封装和重用常见构建逻辑的方法。它们允许您为项目定义一组约定,然后将这些约定应用到其他项目或模块。
上面的示例已被重写为存储在的约定插件buildSrc
:
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.io.File
abstract class CreateFileTask : DefaultTask() {
@get:Input
abstract val fileText: Property<String>
@Input
val fileName = project.rootDir.toString() + "/myfile.txt"
@OutputFile
val myFile: File = File(fileName)
@TaskAction
fun action() {
myFile.createNewFile()
myFile.writeText(fileText.get())
}
}
class MyConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register("createFileTask", CreateFileTask::class.java) {
group = "from my plugin"
description = "Create myfile.txt in the current directory"
fileText.set("HELLO FROM MY PLUGIN")
}
}
}
可以给插件一个id
usinggradlePlugin{}
块,以便可以在根中引用它:
gradlePlugin {
plugins {
create("my-convention-plugin") {
id = "com.gradle.plugin.my-convention-plugin"
implementationClass = "com.gradle.plugin.MyConventionPlugin"
}
}
}
该gradlePlugin{}
块定义了项目正在构建的插件。使用新创建的id
,该插件可以相应地应用到其他构建脚本中:
plugins {
application
id("com.gradle.plugin.my-convention-plugin") // Apply the new plugin
}
构建构建
使用 Gradle 构建项目
构建 Gradle 项目以优化构建性能非常重要。多项目构建是 Gradle 中的标准。
多项目构建由一个根项目和一个或多个子项目组成。 Gradle 可以在一次执行中构建根项目和任意数量的子项目。
项目地点
多项目构建在 Gradle 视为根路径的目录中包含单个根项目:.
。
子项目物理上位于根路径下:./subproject
。
子项目有一个 path,它表示该子项目在多项目构建中的位置。大多数情况下,项目路径与其在文件系统中的位置一致。
项目结构在settings.gradle(.kts)
文件中创建。设置文件必须存在于根目录中。
简单的多项目构建
让我们看一个基本的多项目构建示例,其中包含一个根项目和一个子项目。
根项目名为basic-multiproject
,位于您计算机上的某个位置。从Gradle的角度来看,根目录是顶级目录.
。
该项目包含一个名为的子项目./app
:
.
├── app
│ ...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
└── settings.gradle
这是启动任何 Gradle 项目的推荐项目结构。 build init 插件还会生成遵循此结构的骨架项目 - 具有单个子项目的根项目:
该settings.gradle(.kts)
文件向 Gradle 描述了项目结构:
rootProject.name = "basic-multiproject"
include("app")
rootProject.name = 'basic-multiproject'
include 'app'
app
在这种情况下,Gradle 将在目录中查找子项目的构建文件./app
。
您可以通过运行以下命令查看多项目构建的结构projects
:
$ ./gradlew -q projects ------------------------------------------------------------ Root project 'basic-multiproject' ------------------------------------------------------------ Root project 'basic-multiproject' \--- Project ':app' To see a list of the tasks of a project, run gradle <project-path>:tasks For example, try running gradle :app:tasks
在此示例中,app
子项目是一个 Java 应用程序,它应用应用程序插件并配置主类。应用程序打印Hello World
到控制台:
plugins {
id("application")
}
application {
mainClass = "com.example.Hello"
}
plugins {
id 'application'
}
application {
mainClass = 'com.example.Hello'
}
package com.example;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
run
您可以通过从项目根目录中的应用程序插件执行任务来运行应用程序:
$ ./gradlew -q run Hello, world!
添加子项目
在设置文件中,可以使用以下include
方法将另一个子项目添加到根项目中:
include("project1", "project2:child1", "project3:child1")
include 'project1', 'project2:child1', 'project3:child1'
该include
方法将项目路径作为参数。假定项目路径等于相对物理文件系统路径。例如,路径services:api
默认映射到文件夹./services/api
(相对于项目 root .
)。
有关如何使用项目路径的更多示例可以在Settings.include(java.lang.String[])的 DSL 文档中找到。
让我们添加另一个子项目,lib
调用之前创建的项目。
我们需要做的就是include
在根设置文件中添加另一条语句:
rootProject.name = "basic-multiproject"
include("app")
include("lib")
rootProject.name = 'basic-multiproject'
include 'app'
include 'lib'
lib
然后 Gradle 将在目录中查找新子项目的构建文件./lib/
:
.
├── app
│ ...
│ └── build.gradle.kts
├── lib
│ ...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
├── lib
│ ...
│ └── build.gradle
└── settings.gradle
项目描述
为了进一步向 Gradle 描述项目架构,设置文件提供了项目描述符。
您可以随时在设置文件中修改这些描述符。
要访问描述符,您可以:
include("project-a")
println(rootProject.name)
println(project(":project-a").name)
include('project-a')
println rootProject.name
println project(':project-a').name
使用此描述符,您可以更改项目的名称、项目目录和构建文件:
rootProject.name = "main"
include("project-a")
project(":project-a").projectDir = file("custom/my-project-a")
project(":project-a").buildFileName = "project-a.gradle.kts"
rootProject.name = 'main'
include('project-a')
project(':project-a').projectDir = file('custom/my-project-a')
project(':project-a').buildFileName = 'project-a.gradle'
有关详细信息,请参阅API 文档中的ProjectDescriptor类。
修改子项目路径
让我们假设一个具有以下结构的项目:
.
├── app
│ ...
│ └── build.gradle.kts
├── subs // Gradle may see this as a subproject
│ └── web // Gradle may see this as a subproject
│ └── my-web-module // Intended subproject
│ ...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
├── subs // Gradle may see this as a subproject
│ └── web // Gradle may see this as a subproject
│ └── my-web-module // Intended subproject
│ ...
│ └── build.gradle
└── settings.gradle
如果你settings.gradle(.kts)
看起来像这样:
include(':subs:web:my-web-module')
Gradle 看到一个子项目的逻辑项目名称为 ,:subs:web:my-web-module
并且两个(可能是无意的)其他子项目的逻辑名称为:subs
和:subs:web
。这可能会导致幻像构建目录,特别是在使用allprojects{}
或时subproject{}
。
为了避免这种情况,您可以使用:
include(':subs:web:my-web-module')
project(':subs:web:my-web-module').projectDir = "subs/web/my-web-module"
这样您最终只会得到一个名为 的子项目:subs:web:my-web-module
。
或者您可以使用:
include(':my-web-module')
project(':my-web-module').projectDir = "subs/web/my-web-module"
这样您最终只会得到一个名为 的子项目:my-web-module
。
因此,虽然物理项目布局相同,但逻辑结果却不同。
命名建议
随着项目的发展,命名和一致性变得越来越重要。为了保持您的构建可维护,我们建议如下:
-
保留子项目的默认项目名称:可以在设置文件中配置自定义项目名称。然而,对于开发人员来说,跟踪哪些项目属于哪些文件夹是不必要的额外工作。
-
对所有项目名称使用小写连字符
-
:所有字母均为小写,单词之间用破折号 ( ) 字符分隔。 -
在设置文件中定义根项目名称:
rootProject.name
有效地为构建分配一个名称,用于构建扫描等报告中。如果未设置根项目名称,则该名称将是容器目录名称,这可能不稳定(即,您可以在任何目录中查看您的项目)。如果未设置根项目名称并检出到文件系统的根(例如,/
或C:\
),则该名称将随机生成。
声明子项目之间的依赖关系
如果一个子项目依赖于另一个子项目怎么办?如果一个项目需要另一个项目生成的工件怎么办?
这是多项目构建的常见用例。 Gradle为此提供了项目依赖项。
取决于另一个项目
让我们探索具有以下布局的理论多项目构建:
.
├── api
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── api
│ ├── src
│ │ └──...
│ └── build.gradle
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle
└── settings.gradle
在此示例中,有三个子项目,分别称为shared
、api
和person-service
:
-
该
person-service
子项目依赖于其他两个子项目,shared
和api
。 -
子项目
api
取决于shared
子项目。
我们使用:
分隔符来定义项目路径,例如services:person-service
或:shared
。有关定义项目路径的更多信息,请参阅Settings.include(java.lang.String[])的 DSL 文档。
rootProject.name = "dependencies-java"
include("api", "shared", "services:person-service")
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
}
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
implementation(project(":shared"))
}
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
implementation(project(":shared"))
implementation(project(":api"))
}
rootProject.name = 'basic-dependencies'
include 'api', 'shared', 'services:person-service'
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
}
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
implementation project(':shared')
}
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
implementation project(':shared')
implementation project(':api')
}
项目依赖性会影响执行顺序。它会导致首先构建另一个项目,并将输出与另一个项目的类添加到类路径中。它还将其他项目的依赖项添加到类路径中。
如果执行./gradlew :api:compile
,则首先shared
构建项目,然后api
构建项目。
取决于另一个项目生成的工件
有时,您可能希望依赖另一个项目中特定任务的输出,而不是整个项目。然而,不鼓励显式声明从一个项目到另一个项目的任务依赖关系,因为它会引入任务之间不必要的耦合。
对依赖项进行建模的推荐方法是生成输出并将其标记为“传出”工件,其中一个项目中的任务依赖于另一个项目的输出。 Gradle 的依赖管理引擎允许您在项目之间共享任意工件并按需构建它们。
在子项目之间共享构建逻辑
多项目构建中的子项目通常共享一些共同的依赖关系。
Gradle 提供了一个特殊的目录来存储可自动应用于子项目的共享构建逻辑,而不是在每个子项目构建脚本中复制和粘贴相同的 Java 版本和库。
共享逻辑buildSrc
buildSrc
是 Gradle 识别和受保护的目录,它具有一些优点:
-
可重用的构建逻辑:
buildSrc
允许您以结构化方式组织和集中您的自定义构建逻辑、任务和插件。在 buildSrc 中编写的代码可以在您的项目中重复使用,从而更轻松地维护和共享常见的构建功能。 -
与主构建的隔离:
放置的代码
buildSrc
与项目的其他构建脚本隔离。这有助于保持主要构建脚本更清晰并更专注于特定于项目的配置。 -
自动编译和类路径:
该目录的内容
buildSrc
会自动编译并包含在主构建的类路径中。这意味着 buildSrc 中定义的类和插件可以直接在项目的构建脚本中使用,无需任何额外的配置。 -
易于测试:
由于
buildSrc
是单独的构建,因此它可以轻松测试您的自定义构建逻辑。您可以为构建代码编写测试,确保其行为符合预期。 -
Gradle 插件开发:
如果您正在为您的项目开发自定义 Gradle 插件,那么
buildSrc
这是一个存放插件代码的方便位置。这使得插件可以在您的项目中轻松访问。
该buildSrc
目录被视为包含的构建。
对于多项目构建,只能有一个buildSrc
目录,并且必须位于项目根目录中。
笔记
|
使用它的缺点buildSrc 是,对其进行任何更改都会使项目中的每个任务无效并需要重新运行。
|
buildSrc
使用适用于 Java、Groovy 和 Kotlin 项目的相同源代码约定。它还提供对 Gradle API 的直接访问。
一个典型的项目包括buildSrc
以下布局:
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──kotlin
│ │ └──MyCustomTask.kt // (1)
│ ├── shared.gradle.kts // (2)
│ └── build.gradle.kts
├── api
│ ├── src
│ │ └──...
│ └── build.gradle.kts // (3)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle.kts // (3)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle.kts
└── settings.gradle.kts
-
Create the
MyCustomTask
task. -
A shared build script.
-
Uses the
MyCustomTask
task and shared build script.
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──kotlin
│ │ └──MyCustomTask.groovy // (1)
│ ├── shared.gradle // (2)
│ └── build.gradle
├── api
│ ├── src
│ │ └──...
│ └── build.gradle // (3)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle // (3)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle
└── settings.gradle
-
Create the
MyCustomTask
task. -
A shared build script.
-
Uses the
MyCustomTask
task and shared build script.
在 中,创建了buildSrc
构建脚本。shared.gradle(.kts)
它包含多个子项目共有的依赖项和其他构建信息:
repositories {
mavenCentral()
}
dependencies {
implementation("org.slf4j:slf4j-api:1.7.32")
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.32'
}
在 中buildSrc
,MyCustomTask
还创建了 。它是一个辅助任务,用作多个子项目的构建逻辑的一部分:
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
open class MyCustomTask : DefaultTask() {
@TaskAction
fun calculateSum() {
// Custom logic to calculate the sum of two numbers
val num1 = 5
val num2 = 7
val sum = num1 + num2
// Print the result
println("Sum: $sum")
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
class MyCustomTask extends DefaultTask {
@TaskAction
void calculateSum() {
// Custom logic to calculate the sum of two numbers
int num1 = 5
int num2 = 7
int sum = num1 + num2
// Print the result
println "Sum: $sum"
}
}
该任务在和项目MyCustomTask
的构建脚本中使用。该任务自动可用,因为它是.api
shared
buildSrc
该shared.build(.kts)
文件也适用:
// Apply any other configurations specific to your project
// Use the build script defined in buildSrc
apply(from = rootProject.file("buildSrc/shared.gradle"))
// Use the custom task defined in buildSrc
tasks.register<MyCustomTask>("myCustomTask")
// Apply any other configurations specific to your project
// Use the build script defined in buildSrc
apply from: rootProject.file('buildSrc/shared.gradle')
// Use the custom task defined in buildSrc
tasks.register('myCustomTask', MyCustomTask)
使用约定插件共享逻辑
Gradle 组织构建逻辑的推荐方法是使用其插件系统。
我们可以编写一个插件来封装一个项目中多个子项目通用的构建逻辑。这种插件称为约定插件。
虽然编写插件超出了本节的范围,但构建 Gradle 项目的推荐方法是将通用构建逻辑放入位于buildSrc
.
让我们看一个示例项目:
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──kotlin
│ │ └──myproject.java-conventions.gradle // (1)
│ └── build.gradle.kts
├── api
│ ├── src
│ │ └──...
│ └── build.gradle.kts // (2)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle.kts // (2)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle.kts // (2)
└── settings.gradle.kts
-
Create the
myproject.java-conventions
convention plugin. -
Applies the
myproject.java-conventions
convention plugin.
.
├── buildSrc
│ ├── src
│ │ └──main
│ │ └──kotlin
│ │ └──myproject.java-conventions.gradle.kts // (1)
│ └── build.gradle
├── api
│ ├── src
│ │ └──...
│ └── build.gradle // (2)
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle // (2)
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle // (2)
└── settings.gradle
-
Create the
myproject.java-conventions
convention plugin. -
Applies the
myproject.java-conventions
convention plugin.
该构建包含三个子项目:
rootProject.name = "dependencies-java"
include("api", "shared", "services:person-service")
rootProject.name = 'dependencies-java'
include 'api', 'shared', 'services:person-service'
目录中创建的约定插件源码buildSrc
如下:
plugins {
id("java")
}
group = "com.example"
version = "1.0"
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
}
plugins {
id 'java'
}
group = 'com.example'
version = '1.0'
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
}
约定插件应用于api
、shared
和person-service
子项目:
plugins {
id("myproject.java-conventions")
}
dependencies {
implementation(project(":shared"))
}
plugins {
id("myproject.java-conventions")
}
plugins {
id("myproject.java-conventions")
}
dependencies {
implementation(project(":shared"))
implementation(project(":api"))
}
plugins {
id 'myproject.java-conventions'
}
dependencies {
implementation project(':shared')
}
plugins {
id 'myproject.java-conventions'
}
plugins {
id 'myproject.java-conventions'
}
dependencies {
implementation project(':shared')
implementation project(':api')
}
不要使用跨项目配置
在子项目之间共享构建逻辑的一种不正确的方法是通过和DSL 构造进行跨项目配置。subprojects {}
allprojects {}
提示
|
避免使用subprojects {} 和allprojects {} 。
|
通过交叉配置,构建逻辑可以注入到子项目中,这在查看其构建脚本时并不明显。
从长远来看,交叉配置通常会变得更加复杂并成为一种负担。交叉配置还可能在项目之间引入配置时耦合,这可能会阻止按需配置等优化正常工作。
复合构建
复合构建是包含其他构建的构建。
复合构建类似于 Gradle 多项目构建,只不过不是包含,而是包含subprojects
整个。builds
复合构建允许您:
-
组合通常独立开发的构建,例如,在应用程序使用的库中尝试修复错误时。
-
将大型多项目构建分解为更小、更独立的块,这些块可以根据需要独立或一起工作。
包含在复合构建中的构建称为包含构建。包含的构建不与复合构建或其他包含的构建共享任何配置。每个包含的构建都是独立配置和执行的。
定义复合构建
以下示例演示了如何将两个通常单独开发的 Gradle 构建组合成一个复合构建。
my-composite
├── gradle
├── gradlew
├── settings.gradle.kts
├── build.gradle.kts
├── my-app
│ ├── settings.gradle.kts
│ └── app
│ ├── build.gradle.kts
│ └── src/main/java/org/sample/my-app/Main.java
└── my-utils
├── settings.gradle.kts
├── number-utils
│ ├── build.gradle.kts
│ └── src/main/java/org/sample/numberutils/Numbers.java
└── string-utils
├── build.gradle.kts
└── src/main/java/org/sample/stringutils/Strings.java
多my-utils
项目构建生成两个 Java 库,number-utils
以及string-utils
.构建my-app
使用这些库中的函数生成可执行文件。
构建my-app
不直接依赖于my-utils
.相反,它声明对以下生成的库的二进制依赖关系my-utils
:
plugins {
id("application")
}
application {
mainClass = "org.sample.myapp.Main"
}
dependencies {
implementation("org.sample:number-utils:1.0")
implementation("org.sample:string-utils:1.0")
}
plugins {
id 'application'
}
application {
mainClass = 'org.sample.myapp.Main'
}
dependencies {
implementation 'org.sample:number-utils:1.0'
implementation 'org.sample:string-utils:1.0'
}
通过定义复合构建--include-build
命令--include-build
行参数将执行的构建转换为复合构建,将包含的构建中的依赖项替换为执行的构建。
./gradlew run --include-build ../my-utils
例如, run from的输出my-app
:
$ ./gradlew --include-build ../my-utils run link:http://gradle.github.net.cn/8.7/samples/build-organization/composite-builds/basic/tests/basicCli.out[role=include]
通过设置文件定义复合构建
通过使用Settings.includeBuild(java.lang.Object)声明文件中包含的构建,可以使上述安排持久化settings.gradle(.kts)
。
设置文件可用于同时添加子项目和包含的构建。
包含的构建按位置添加:
includeBuild("my-utils")
在示例中,settings.gradle(.kts) 文件组合了其他单独的构建:
rootProject.name = "my-composite"
includeBuild("my-app")
includeBuild("my-utils")
rootProject.name = 'my-composite'
includeBuild 'my-app'
includeBuild 'my-utils'
要执行构建run
中的任务,请运行。my-app
my-composite
./gradlew my-app:app:run
您可以选择定义一个依赖于的run
任务,以便您可以执行:my-composite
my-app:app:run
./gradlew run
tasks.register("run") {
dependsOn(gradle.includedBuild("my-app").task(":app:run"))
}
tasks.register('run') {
dependsOn gradle.includedBuild('my-app').task(':app:run')
}
包括定义 Gradle 插件的构建
包含构建的一个特殊情况是定义 Gradle 插件的构建。
应使用设置文件块includeBuild
内的语句包含这些构建。pluginManagement {}
使用这种机制,包含的构建还可以提供一个可以应用于设置文件本身的设置插件:
pluginManagement {
includeBuild("../url-verifier-plugin")
}
pluginManagement {
includeBuild '../url-verifier-plugin'
}
对包含的构建的限制
大多数构建都可以包含在组合中,包括其他组合构建。有一些限制。
在常规构建中,Gradle 确保每个项目都有唯一的项目路径。它使项目可识别和可寻址,而不会发生冲突。
在复合构建中,Gradle 向包含的构建中的每个项目添加额外的资格,以避免项目路径冲突。在复合构建中标识项目的完整路径称为构建树路径。它由包含的构建的构建路径和项目的项目路径组成。
默认情况下,构建路径和项目路径源自磁盘上的目录名称和结构。由于包含的构建可以位于磁盘上的任何位置,因此它们的构建路径由包含目录的名称确定。这有时会导致冲突。
总而言之,包含的版本必须满足以下要求:
-
每个包含的构建都必须具有唯一的构建路径。
-
每个包含的构建路径不得与主构建的任何项目路径冲突。
这些条件保证了即使在复合构建中也可以唯一地标识每个项目。
如果出现冲突,解决它们的方法是更改包含的构建的构建名称:
includeBuild("some-included-build") {
name = "other-name"
}
笔记
|
当一个复合构建包含在另一个复合构建中时,两个构建具有相同的父代。换句话说,嵌套的复合构建结构被展平了。 |
与复合构建交互
与复合构建的交互通常类似于常规的多项目构建。可以执行任务、运行测试并将构建导入到 IDE 中。
执行任务
包含的构建中的任务可以从命令行或 IDE 执行,其方式与常规多项目构建中的任务相同。执行任务将导致执行任务依赖项,以及从其他包含的构建构建依赖项工件所需的那些任务。
您可以使用完全限定路径(例如:included-build-name:project-name:taskName
.项目和任务名称可以缩写。
$ ./gradlew :included-build:subproject-a:compileJava > Task :included-build:subproject-a:compileJava $ ./gradlew :i-b:sA:cJ > Task :included-build:subproject-a:compileJava
要从命令行中排除任务,您需要提供该任务的完全限定路径。
笔记
|
包含的构建任务会自动执行以生成所需的依赖项工件,或者包含的构建可以声明对包含的构建中的任务的依赖关系。 |
导入IDE
复合构建最有用的功能之一是 IDE 集成。
导入复合构建允许来自单独 Gradle 构建的源轻松地一起开发。对于每个包含的构建,每个子项目都作为 IntelliJ IDEA 模块或 Eclipse 项目包含在内。配置源依赖项,提供跨构建导航和重构。
声明由包含的构建替换的依赖项
默认情况下,Gradle 将配置每个包含的构建以确定它可以提供的依赖项。执行此操作的算法很简单。 Gradle 将检查包含的构建中项目的组和名称,并替换任何外部依赖项匹配的项目依赖项${project.group}:${project.name}
。
笔记
|
默认情况下,不会为主构建注册替换。 要使主构建的(子)项目可通过 寻址 |
在某些情况下,Gradle 确定的默认替换是不够的,或者必须针对特定组合进行更正。对于这些情况,可以明确声明所包含构建的替换。
例如,名为 的单项目构建anonymous-library
会生成一个 Java 实用程序库,但不会声明 group 属性的值:
plugins {
java
}
plugins {
id 'java'
}
当此构建包含在组合中时,它将尝试替换依赖模块undefined:anonymous-library
(undefined
是 的默认值project.group
,并且anonymous-library
是根项目名称)。显然,这在复合构建中没有用。
要在复合构建中使用未发布的库,您可以显式声明它提供的替换:
includeBuild("anonymous-library") {
dependencySubstitution {
substitute(module("org.sample:number-utils")).using(project(":"))
}
}
includeBuild('anonymous-library') {
dependencySubstitution {
substitute module('org.sample:number-utils') using project(':')
}
}
通过此配置,my-app
复合构建将用org.sample:number-utils
对 的根项目的依赖项替换对 的任何依赖项anonymous-library
。
停用配置的包含构建替换
如果您需要解析也可作为包含的构建的一部分提供的模块的已发布版本,则可以在已解析的配置的ResolutionStrategy上停用包含的构建替换规则。这是必要的,因为规则在构建中全局应用,并且默认情况下,Gradle 在解析过程中不考虑已发布的版本。
例如,我们创建一个单独的publishedRuntimeClasspath
配置,该配置被解析为也存在于本地构建之一中的模块的已发布版本。这是通过停用全局依赖替换规则来完成的:
configurations.create("publishedRuntimeClasspath") {
resolutionStrategy.useGlobalDependencySubstitutionRules = false
extendsFrom(configurations.runtimeClasspath.get())
isCanBeConsumed = false
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}
configurations.create('publishedRuntimeClasspath') {
resolutionStrategy.useGlobalDependencySubstitutionRules = false
extendsFrom(configurations.runtimeClasspath)
canBeConsumed = false
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
}
一个用例是比较已发布的 JAR 文件和本地构建的 JAR 文件。
必须声明包含的构建替换的情况
许多构建将作为包含的构建自动运行,无需声明替换。以下是一些需要声明替换的常见情况:
-
当该
archivesBaseName
属性用于设置已发布工件的名称时。 -
default
当发布以外的配置时。 -
当
MavenPom.addFilter()
用于发布与项目名称不匹配的工件时。 -
当使用
maven-publish
或ivy-publish
插件进行发布且发布坐标不匹配时${project.group}:${project.name}
。
复合构建替换不起作用的情况
某些构建在包含在组合中时将无法正常运行,即使显式声明了依赖项替换也是如此。此限制是因为替换的项目依赖项将始终指向default
目标项目的配置。每当为项目的默认配置指定的工件和依赖项与发布到存储库的内容不匹配时,复合构建可能会表现出不同的行为。
以下是发布的模块元数据可能与项目默认配置不同的一些情况:
-
default
当发布以外的配置时。 -
当使用
maven-publish
或ivy-publish
插件时。 -
当
POM
或ivy.xml
文件作为发布的一部分进行调整时。
当包含在复合构建中时,使用这些功能的构建无法正常运行。
取决于包含的构建中的任务
虽然包含的构建彼此隔离并且无法声明直接依赖关系,但复合构建可以声明其包含的构建的任务依赖关系。使用Gradle.getIncludedBuilds()或Gradle.includedBuild(java.lang.String)访问包含的构建,并通过IncludedBuild.task(java.lang.String)方法获取任务引用。
使用这些 API,可以声明对特定包含的构建中的任务的依赖关系:
tasks.register("run") {
dependsOn(gradle.includedBuild("my-app").task(":app:run"))
}
tasks.register('run') {
dependsOn gradle.includedBuild('my-app').task(':app:run')
}
或者,您可以在部分或全部包含的构建中声明对具有特定路径的任务的依赖关系:
tasks.register("publishDeps") {
dependsOn(gradle.includedBuilds.map { it.task(":publishMavenPublicationToMavenRepository") })
}
tasks.register('publishDeps') {
dependsOn gradle.includedBuilds*.task(':publishMavenPublicationToMavenRepository')
}
复合构建的局限性
当前实施的局限性包括:
-
不支持包含不反映项目默认配置的发布的构建。
请参阅复合构建不起作用的情况。 -
如果多个复合构建包含相同的构建,则并行运行时多个复合构建可能会发生冲突。
Gradle 不会在 Gradle 调用之间共享共享复合构建的项目锁,以防止并发执行。
按需配置
按需配置尝试仅为所请求的任务配置相关项目,即,它仅评估参与构建的项目的构建脚本文件。这样,可以减少大型多项目构建的配置时间。
按需配置功能正在孵化中,因此仅保证某些构建能够正常工作。该功能非常适合解耦的多项目构建。
按需配置模式下,项目配置如下:
-
根项目始终已配置。
-
执行构建的目录中的项目也已配置,但仅限于在没有任何任务的情况下执行 Gradle 时。
这样,当按需配置项目时,默认任务就会正确运行。 -
支持标准项目依赖,并配置相关项目。
如果项目 A 对项目 B 有编译依赖性,则构建 A 会导致两个项目的配置。 -
支持通过任务路径声明的任务依赖项,并导致配置相关项目。
例子:someTask.dependsOn(":some-other-project:someOtherTask")
-
从命令行(或工具 API)通过任务路径请求的任务会导致相关项目被配置。
例如,构建project-a:project-b:someTask
会导致project-b
.
使能够configuration-on-demand
--configure-on-demand
您可以使用该标志或添加org.gradle.configureondemand=true
到文件来启用按需配置gradle.properties
。
要在每次构建运行时按需配置,请参阅Gradle 属性。
要根据给定构建的需要进行配置,请参阅命令行面向性能的选项。
解耦项目
当项目仅通过声明的依赖项和任务依赖项进行交互时,它们被认为是解耦的。对另一个项目对象的任何直接修改或读取都会在项目之间创建耦合。使用“按需配置”时,配置期间的耦合可能会导致有缺陷的构建结果,而执行期间的耦合可能会影响并行执行。
耦合的一种常见来源是配置注入,例如使用allprojects{}
或subprojects{}
在构建脚本中。
为避免耦合问题,建议:
-
避免引用其他子项目的构建脚本,并且更喜欢从根项目进行交叉配置。
-
避免在执行过程中动态更改其他项目的配置。
随着 Gradle 的发展,它的目标是提供利用解耦项目的功能,同时为配置注入等常见用例提供解决方案,而无需引入耦合。
开发任务
了解任务
任务代表构建执行的某些独立工作单元,例如编译类、创建 JAR、生成 Javadoc 或将存档发布到存储库。
列出任务
项目中的所有可用任务都来自 Gradle 插件和构建脚本。
您可以通过在终端中运行以下命令来列出项目中的所有可用任务:
$ ./gradlew tasks
让我们以一个非常基本的 Gradle 项目为例。该项目具有以下结构:
gradle-project
├── app
│ ├── build.gradle.kts // empty file - no build logic
│ └── ... // some java code
├── settings.gradle.kts // includes app subproject
├── gradle
│ └── ...
├── gradlew
└── gradlew.bat
gradle-project
├── app
│ ├── build.gradle // empty file - no build logic
│ └── ... // some java code
├── settings.gradle // includes app subproject
├── gradle
│ └── ...
├── gradlew
└── gradlew.bat
设置文件包含以下内容:
rootProject.name = "gradle-project"
include("app")
rootProject.name = 'gradle-project'
include('app')
目前,app
子项目的构建文件为空。
要查看子项目中可用的任务app
,请运行./gradlew :app:tasks
:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in project ':app'.
dependencies - Displays all dependencies declared in project ':app'.
dependencyInsight - Displays the insight into a specific dependency in project ':app'.
help - Displays a help message.
javaToolchains - Displays the detected java toolchains.
kotlinDslAccessorsReport - Prints the Kotlin code for accessing the currently available project extensions and conventions.
outgoingVariants - Displays the outgoing variants of project ':app'.
projects - Displays the sub-projects of project ':app'.
properties - Displays the properties of project ':app'.
resolvableConfigurations - Displays the configurations that can be resolved in project ':app'.
tasks - Displays the tasks runnable from project ':app'.
我们观察到,目前只有少量的帮助任务可用。这是因为 Gradle 的核心仅提供分析构建的任务。其他任务,例如构建项目或编译代码的任务,是通过插件添加的。
让我们通过将Gradle 核心base
插件添加到app
构建脚本来探索这一点:
plugins {
id("base")
}
plugins {
id('base')
}
该base
插件添加了中心生命周期任务。现在,当我们运行时./gradlew app:tasks
,我们可以看到assemble
和build
任务可用:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
clean - Deletes the build directory.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in project ':app'.
dependencies - Displays all dependencies declared in project ':app'.
dependencyInsight - Displays the insight into a specific dependency in project ':app'.
help - Displays a help message.
javaToolchains - Displays the detected java toolchains.
outgoingVariants - Displays the outgoing variants of project ':app'.
projects - Displays the sub-projects of project ':app'.
properties - Displays the properties of project ':app'.
resolvableConfigurations - Displays the configurations that can be resolved in project ':app'.
tasks - Displays the tasks runnable from project ':app'.
Verification tasks
------------------
check - Runs all checks.
任务组和描述
任务组和描述用于组织和描述任务。
- 团体
-
任务组用于对任务进行分类。当您运行时
./gradlew tasks
,任务会在各自的组下列出,从而更容易理解它们的目的以及与其他任务的关系。组是使用该group
属性设置的。 - 描述
-
描述提供任务用途的简要说明。当您运行时
./gradlew tasks
,每个任务旁边都会显示说明,帮助您了解其目的以及如何使用它。描述是使用description
属性设置的。
让我们以一个基本的 Java 应用程序为例。该构建包含一个名为 的子项目app
。
app
让我们列出目前可用的任务:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Application tasks
-----------------
run - Runs this project as a JVM application.
Build tasks
-----------
assemble - Assembles the outputs of this project.
在这里,任务是具有描述的组:run
的一部分。在代码中,它看起来像这样:Application
Runs this project as a JVM application
tasks.register("run") {
group = "Application"
description = "Runs this project as a JVM application."
}
tasks.register("run") {
group = "Application"
description = "Runs this project as a JVM application."
}
私人和隐藏任务
Gradle 不支持将任务标记为private。
:tasks
但是,只有在task.group
设置了该值或者没有其他任务依赖于它时,任务才会在运行时显示。
例如,以下任务在运行时不会出现,./gradlew :app:tasks
因为它没有组;它被称为隐藏任务:
tasks.register("helloTask") {
println("Hello")
}
tasks.register("helloTask") {
println("Hello")
}
虽然helloTask
没有列出,但仍然可以通过Gradle执行:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Application tasks
-----------------
run - Runs this project as a JVM application
Build tasks
-----------
assemble - Assembles the outputs of this project.
让我们向同一任务添加一个组:
tasks.register("helloTask") {
group = "Other"
description = "Hello task"
println("Hello")
}
tasks.register("helloTask") {
group = "Other"
description = "Hello task"
println("Hello")
}
现在组已添加,任务可见:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Application tasks
-----------------
run - Runs this project as a JVM application
Build tasks
-----------
assemble - Assembles the outputs of this project.
Other tasks
-----------
helloTask - Hello task
反之,./gradlew tasks --all
将显示所有任务;列出了隐藏和可见的任务。
任务分组
如果您想自定义在列出时向用户显示哪些任务,您可以对任务进行分组并设置每个组的可见性。
笔记
|
请记住,即使您隐藏任务,它们仍然可用,并且 Gradle 仍然可以运行它们。 |
init
让我们从 Gradle为具有多个子项目的 Java 应用程序构建的示例开始。项目结构如下:
gradle-project
├── app
│ ├── build.gradle.kts
│ └── src // some java code
│ └── ...
├── utilities
│ ├── build.gradle.kts
│ └── src // some java code
│ └── ...
├── list
│ ├── build.gradle.kts
│ └── src // some java code
│ └── ...
├── buildSrc
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src // common build logic
│ └── ...
├── settings.gradle.kts
├── gradle
├── gradlew
└── gradlew.bat
gradle-project
├── app
│ ├── build.gradle
│ └── src // some java code
│ └── ...
├── utilities
│ ├── build.gradle
│ └── src // some java code
│ └── ...
├── list
│ ├── build.gradle
│ └── src // some java code
│ └── ...
├── buildSrc
│ ├── build.gradle
│ ├── settings.gradle
│ └── src // common build logic
│ └── ...
├── settings.gradle
├── gradle
├── gradlew
└── gradlew.bat
运行app:tasks
以查看app
子项目中的可用任务:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Application tasks
-----------------
run - Runs this project as a JVM application
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the classes of the 'main' feature.
testClasses - Assembles test classes.
Distribution tasks
------------------
assembleDist - Assembles the main distributions
distTar - Bundles the project as a distribution.
distZip - Bundles the project as a distribution.
installDist - Installs the project as a distribution as-is.
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the 'main' feature.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in project ':app'.
dependencies - Displays all dependencies declared in project ':app'.
dependencyInsight - Displays the insight into a specific dependency in project ':app'.
help - Displays a help message.
javaToolchains - Displays the detected java toolchains.
kotlinDslAccessorsReport - Prints the Kotlin code for accessing the currently available project extensions and conventions.
outgoingVariants - Displays the outgoing variants of project ':app'.
projects - Displays the sub-projects of project ':app'.
properties - Displays the properties of project ':app'.
resolvableConfigurations - Displays the configurations that can be resolved in project ':app'.
tasks - Displays the tasks runnable from project ':app'.
Verification tasks
------------------
check - Runs all checks.
test - Runs the test suite.
如果我们查看可用任务列表,即使对于标准 Java 项目,也会发现它非常广泛。使用构建的开发人员很少直接需要其中许多任务。
我们可以配置:tasks
任务并将任务限制为特定组显示。
让我们创建自己的组,以便通过更新构建脚本默认隐藏所有任务app
:
val myBuildGroup = "my app build" // Create a group name
tasks.register<TaskReportTask>("tasksAll") { // Register the tasksAll task
group = myBuildGroup
description = "Show additional tasks."
setShowDetail(true)
}
tasks.named<TaskReportTask>("tasks") { // Move all existing tasks to the group
displayGroup = myBuildGroup
}
def myBuildGroup = "my app build" // Create a group name
tasks.register(TaskReportTask, "tasksAll") { // Register the tasksAll task
group = myBuildGroup
description = "Show additional tasks."
setShowDetail(true)
}
tasks.named(TaskReportTask, "tasks") { // Move all existing tasks to the group
displayGroup = myBuildGroup
}
现在,当我们列出 中可用的任务时app
,列表会更短:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
My app build tasks
------------------
tasksAll - Show additional tasks.
任务类别
Gradle 区分两类任务:
-
生命周期任务
-
可操作的任务
生命周期任务定义您可以调用的目标,例如:build
您的项目。生命周期任务不向 Gradle 提供操作。他们必须连接到可操作的任务。 Gradlebase
插件仅添加生命周期任务。
可操作任务定义 Gradle 要执行的操作,例如:compileJava
编译项目的 Java 代码。操作包括创建 JAR、压缩文件、发布档案等等。像java-library
插件这样的插件添加了可操作的任务。
让我们更新上一个示例的构建脚本,该脚本当前是一个空文件,以便我们的app
子项目是一个 Java 库:
plugins {
id("java-library")
}
plugins {
id('java-library')
}
我们再次列出可用的任务,看看有哪些新任务可用:
$ ./gradlew :app:tasks
> Task :app:tasks
------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the classes of the 'main' feature.
testClasses - Assembles test classes.
Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the 'main' feature.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in project ':app'.
dependencies - Displays all dependencies declared in project ':app'.
dependencyInsight - Displays the insight into a specific dependency in project ':app'.
help - Displays a help message.
javaToolchains - Displays the detected java toolchains.
outgoingVariants - Displays the outgoing variants of project ':app'.
projects - Displays the sub-projects of project ':app'.
properties - Displays the properties of project ':app'.
resolvableConfigurations - Displays the configurations that can be resolved in project ':app'.
tasks - Displays the tasks runnable from project ':app'.
Verification tasks
------------------
check - Runs all checks.
test - Runs the test suite.
我们看到有许多新任务可用,例如jar
和testClasses
。
此外,该java-library
插件还将可操作的任务连接到生命周期任务。如果我们调用该:build
任务,我们可以看到包括该任务在内的多个任务已被执行:app:compileJava
。
$./gradlew :app:build
> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes
> Task :app:jar
> Task :app:assemble
> Task :app:compileTestJava
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses
> Task :app:test
> Task :app:check
> Task :app:build
可操作:compileJava
任务连接到生命周期:build
任务。
增量任务
Gradle 任务的一个关键特征是其增量性质。
Gradle 可以重用之前构建的结果。因此,如果我们之前已经构建了项目并且仅进行了较小的更改,则重新运行:build
将不需要 Gradle 执行大量工作。
例如,如果我们只修改项目中的测试代码,而生产代码保持不变,则执行构建只会重新编译测试代码。 Gradle 将生产代码的任务标记为UP-TO-DATE
,表示自上次成功构建以来它保持不变:
$./gradlew :app:build
lkassovic@MacBook-Pro temp1 % ./gradlew :app:build
> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:assemble UP-TO-DATE
> Task :app:compileTestJava
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses
> Task :app:test
> Task :app:check UP-TO-DATE
> Task :app:build UP-TO-DATE
缓存任务
Gradle 可以使用构建缓存重用过go构建的结果。
--build-cache
要启用此功能,请使用命令行org.gradle.caching=true
参数或在文件中设置来激活构建缓存gradle.properties
。
此优化有可能显着加速您的构建:
$./gradlew :app:clean :app:build --build-cache
> Task :app:compileJava FROM-CACHE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar
> Task :app:assemble
> Task :app:compileTestJava FROM-CACHE
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses UP-TO-DATE
> Task :app:test FROM-CACHE
> Task :app:check UP-TO-DATE
> Task :app:build
当 Gradle 可以从缓存中获取任务的输出时,它会使用 来标记该任务FROM-CACHE
。
如果您定期在分支之间切换,构建缓存会很方便。 Gradle 支持本地和远程构建缓存。
开发任务
开发 Gradle 任务时,您有两种选择:
-
使用现有的 Gradle 任务类型,例如
Zip
、Copy
或Delete
-
创建您自己的 Gradle 任务类型,例如
MyResolveTask
或CustomTaskUsingToolchains
。
任务类型只是 GradleTask
类的子类。
对于 Gradle 任务,需要考虑三种状态:
-
注册任务 - 在构建逻辑中使用任务(由您实现或由 Gradle 提供)。
-
配置任务 - 定义已注册任务的输入和输出。
-
实现任务 - 创建自定义任务类(即自定义类类型)。
注册通常是用该register()
方法完成的。
配置任务通常使用该named()
方法完成。
实现任务通常是通过扩展 Gradle 的DefaultTask
类来完成的:
tasks.register<Copy>("myCopy") // (1)
tasks.named<Copy>("myCopy") { // (2)
from("resources")
into("target")
include("**/*.txt", "**/*.xml", "**/*.properties")
}
abstract class MyCopyTask : DefaultTask() { // (3)
@TaskAction
fun copyFiles() {
val sourceDir = File("sourceDir")
val destinationDir = File("destinationDir")
sourceDir.listFiles()?.forEach { file ->
if (file.isFile && file.extension == "txt") {
file.copyTo(File(destinationDir, file.name))
}
}
}
}
-
Register the
myCopy
task of typeCopy
to let Gradle know we intend to use it in our build logic. -
Configure the registered
myCopy
task with the inputs and outputs it needs according to its API. -
Implement a custom task type called
MyCopyTask
which extendsDefaultTask
and defines thecopyFiles
task action.
tasks.register(Copy, "myCopy") // (1)
tasks.named(Copy, "myCopy") { // (2)
from "resources"
into "target"
include "**/*.txt", "**/*.xml", "**/*.properties"
}
abstract class MyCopyTask extends DefaultTask { // (3)
@TaskAction
void copyFiles() {
fileTree('sourceDir').matching {
include '**/*.txt'
}.forEach { file ->
file.copyTo(file.path.replace('sourceDir', 'destinationDir'))
}
}
}
-
Register the
myCopy
task of typeCopy
to let Gradle know we intend to use it in our build logic. -
Configure the registered
myCopy
task with the inputs and outputs it needs according to its API. -
Implement a custom task type called
MyCopyTask
which extendsDefaultTask
and defines thecopyFiles
task action.
1. 注册任务
您可以通过在构建脚本或插件中注册任务来定义 Gradle 要执行的操作。
任务是使用任务名称字符串定义的:
tasks.register("hello") {
doLast {
println("hello")
}
}
tasks.register('hello') {
doLast {
println 'hello'
}
}
在上面的示例中,任务被添加到TasksCollection
usingregister()
中的方法中TaskContainer
。
2. 配置任务
必须配置 Gradle 任务才能成功完成其操作。如果任务需要压缩文件,则必须配置文件名和位置。您可以参考Gradle 任务的APIZip
以了解如何正确配置它。
我们Copy
以 Gradle 提供的任务为例。我们首先在构建脚本中注册一个名为myCopy
of 类型的任务:Copy
tasks.register<Copy>("myCopy")
tasks.register('myCopy', Copy)
这会注册一个没有默认行为的复制任务。由于任务的类型是Copy
Gradle 支持的任务类型,因此可以使用其API进行配置。
以下示例展示了实现相同配置的几种方法:
1、使用named()
方法:
用于named()
配置在其他地方注册的现有任务:
tasks.named<Copy>("myCopy") {
from("resources")
into("target")
include("**/*.txt", "**/*.xml", "**/*.properties")
}
tasks.named('myCopy') {
from 'resources'
into 'target'
include('**/*.txt', '**/*.xml', '**/*.properties')
}
2. 使用配置块:
注册任务后立即使用块来配置任务:
tasks.register<Copy>("copy") {
from("resources")
into("target")
include("**/*.txt", "**/*.xml", "**/*.properties")
}
tasks.register('copy', Copy) {
from 'resources'
into 'target'
include('**/*.txt', '**/*.xml', '**/*.properties')
}
3. 将方法命名为 call:
仅 Groovy 支持的一个流行选项是速记符号:
copy {
from("resources")
into("target")
include("**/*.txt", "**/*.xml", "**/*.properties")
}
笔记
|
此选项会破坏任务配置避免,因此不推荐! |
无论选择哪种方法,任务都会配置要复制的文件的名称和文件的位置。
三、落实任务
Gradle 提供了许多任务类型,包括Delete
、Javadoc
、Copy
、Exec
、Tar
和Pmd
。如果 Gradle 没有提供满足您的构建逻辑需求的任务类型,您可以实现自定义任务类型。
要创建自定义任务类,您需要扩展DefaultTask
并使扩展类抽象:
abstract class MyCopyTask extends DefaultTask {
}
abstract class MyCopyTask : DefaultTask() {
}
您可以在实现任务中了解有关开发自定义任务类型的更多信息。
userguide_single.adoc 中未解析的指令 - include::lifecycle_tasks.adoc[leveloffset=+2] userguide_single.adoc 中未解析的指令 - include::actionable_tasks.adoc[leveloffset=+2] :leveloffset: +2
延迟配置任务
随着构建复杂性的增加,很难跟踪了解配置特定值的时间和位置。 Gradle 提供了多种使用惰性配置来管理此问题的方法。
了解惰性属性
Gradle 提供了惰性属性,它会延迟计算属性的值,直到实际需要时才计算。
惰性属性提供了三个主要好处:
-
延迟值解析:允许连接 Gradle 模型,而无需知道属性值何时已知。例如,您可能希望根据扩展的源目录属性设置任务的输入源文件,但在构建脚本或其他插件配置它们之前,扩展属性值是未知的。
-
自动任务依赖性管理:将一个任务的输出连接到另一任务的输入,自动确定任务依赖性。属性实例携带有关哪个任务(如果有)产生其值的信息。构建作者无需担心任务依赖关系与配置更改保持同步。
-
改进的构建性能:避免配置期间的资源密集型工作,从而对构建性能产生积极影响。例如,当配置值来自解析文件但仅在运行功能测试时使用时,使用属性实例捕获该值意味着仅在运行功能测试时(而不是
clean
运行时)解析文件例子)。
Gradle 用两个接口表示惰性属性:
- 提供者
-
代表一个值,只能查询,不能更改。
-
这些类型的属性是只读的。
-
Provider.get()方法返回属性的当前值。
-
可以使用Provider.map(Transformer)从另一个
Provider
创建A。Provider
-
许多其他类型都可以扩展并可以在任何需要的
Provider
地方使用。Provider
-
- 财产
-
表示一个可以查询和更改的值。
-
这些类型的属性是可配置的。
-
Property
扩展Provider
接口。 -
Property.set(T)方法指定属性的值,覆盖可能存在的任何值。
-
Property.set(Provider)方法指定
Provider
属性的值,覆盖可能存在的任何值。这允许您在配置值之前Provider
将实例连接在一起。Property
-
A可以通过工厂方法ObjectFactory.property(Class)
Property
创建。
-
惰性属性旨在传递并仅在需要时进行查询。这通常发生在执行阶段。
下面演示了一个具有可配置greeting
属性和只读message
属性的任务:
abstract class Greeting : DefaultTask() { // (1)
@get:Input
abstract val greeting: Property<String> // (2)
@Internal
val message: Provider<String> = greeting.map { it + " from Gradle" } // (3)
@TaskAction
fun printMessage() {
logger.quiet(message.get())
}
}
tasks.register<Greeting>("greeting") {
greeting.set("Hi") // (4)
greeting = "Hi" // (5)
}
abstract class Greeting extends DefaultTask { // (1)
@Input
abstract Property<String> getGreeting() // (2)
@Internal
final Provider<String> message = greeting.map { it + ' from Gradle' } // (3)
@TaskAction
void printMessage() {
logger.quiet(message.get())
}
}
tasks.register("greeting", Greeting) {
greeting.set('Hi') // (4)
greeting = 'Hi' // (5)
}
-
显示问候语的任务
-
可配置的问候语
-
根据问候语计算的只读属性
-
配置问候语
-
调用 Property.set() 的替代表示法
$ gradle greeting > Task :greeting Hi from Gradle BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
该Greeting
任务具有表示可配置问候语的 type 属性和表示计算的只读消息的Property<String>
type 属性。Provider<String>
该消息是使用以下方法Provider
从问候语创建的;当问候语属性的值发生变化时,它的值会保持最新。Property
map()
创建 Property 或 Provider 实例
Provider
它及其子类型(例如)都不Property
打算由构建脚本或插件实现。 Gradle 提供了工厂方法来创建这些类型的实例。
在前面的示例中,提供了两个工厂方法:
-
ObjectFactory.property(Class)创建一个新
Property
实例。可以从Project.getObjects()引用或通过构造函数或方法注入来引用ObjectFactory的实例。ObjectFactory
-
Provider.map(Transformer)
Provider
从现有的Provider
或实例创建一个新的Property
。
请参阅快速参考了解所有可用的类型和工厂。
A也可以通过工厂方法ProviderFactory.provider(Callable)Provider
创建。
笔记
|
没有使用 当使用 Groovy 编写插件或构建脚本时,您可以使用 同样,当使用 Kotlin 编写插件或构建脚本时,Kotlin 编译器会将 Kotlin 函数转换为 |
将属性连接在一起
惰性属性的一个重要特征是它们可以连接在一起,以便对一个属性的更改自动反映在其他属性中。
下面是一个示例,其中任务的属性连接到项目扩展的属性:
// A project extension
interface MessageExtension {
// A configurable greeting
abstract val greeting: Property<String>
}
// A task that displays a greeting
abstract class Greeting : DefaultTask() {
// Configurable by the user
@get:Input
abstract val greeting: Property<String>
// Read-only property calculated from the greeting
@Internal
val message: Provider<String> = greeting.map { it + " from Gradle" }
@TaskAction
fun printMessage() {
logger.quiet(message.get())
}
}
// Create the project extension
val messages = project.extensions.create<MessageExtension>("messages")
// Create the greeting task
tasks.register<Greeting>("greeting") {
// Attach the greeting from the project extension
// Note that the values of the project extension have not been configured yet
greeting = messages.greeting
}
messages.apply {
// Configure the greeting on the extension
// Note that there is no need to reconfigure the task's `greeting` property. This is automatically updated as the extension property changes
greeting = "Hi"
}
// A project extension
interface MessageExtension {
// A configurable greeting
Property<String> getGreeting()
}
// A task that displays a greeting
abstract class Greeting extends DefaultTask {
// Configurable by the user
@Input
abstract Property<String> getGreeting()
// Read-only property calculated from the greeting
@Internal
final Provider<String> message = greeting.map { it + ' from Gradle' }
@TaskAction
void printMessage() {
logger.quiet(message.get())
}
}
// Create the project extension
project.extensions.create('messages', MessageExtension)
// Create the greeting task
tasks.register("greeting", Greeting) {
// Attach the greeting from the project extension
// Note that the values of the project extension have not been configured yet
greeting = messages.greeting
}
messages {
// Configure the greeting on the extension
// Note that there is no need to reconfigure the task's `greeting` property. This is automatically updated as the extension property changes
greeting = 'Hi'
}
$ gradle greeting > Task :greeting Hi from Gradle BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
此示例调用Property.set(Provider)方法将 a 附加Provider
到 aProperty
以提供属性的值。在这种情况下,Provider
恰好Property
也是 a,但您可以连接任何Provider
实现,例如使用创建的实现Provider.map()
处理文件
在使用文件中,我们为File
类对象引入了四种集合类型:
只读类型 | 可配置类型 |
---|---|
所有这些类型也被视为惰性类型。
有更强类型的模型用于表示文件系统的元素: Directory和RegularFile。这些类型不应与标准 Java文件类型混淆,因为它们用于告诉 Gradle 您需要更具体的值,例如目录或非目录、常规文件。
Gradle 提供了两个专门的Property
子类型来处理这些类型的值:
RegularFileProperty和DirectoryProperty。ObjectFactory有方法来创建这些:ObjectFactory.fileProperty()和ObjectFactory.directoryProperty()。
ADirectoryProperty
还可用于分别通过DirectoryProperty.dir(String)和DirectoryProperty.file(String)Provider
创建对 a进行延迟计算的Directory
值。这些方法创建的提供程序的值是相对于它们的创建位置来计算的。从这些提供程序返回的值将反映对.RegularFile
DirectoryProperty
DirectoryProperty
// A task that generates a source file and writes the result to an output directory
abstract class GenerateSource : DefaultTask() {
// The configuration file to use to generate the source file
@get:InputFile
abstract val configFile: RegularFileProperty
// The directory to write source files to
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun compile() {
val inFile = configFile.get().asFile
logger.quiet("configuration file = $inFile")
val dir = outputDir.get().asFile
logger.quiet("output dir = $dir")
val className = inFile.readText().trim()
val srcFile = File(dir, "${className}.java")
srcFile.writeText("public class ${className} { }")
}
}
// Create the source generation task
tasks.register<GenerateSource>("generate") {
// Configure the locations, relative to the project and build directories
configFile = layout.projectDirectory.file("src/config.txt")
outputDir = layout.buildDirectory.dir("generated-source")
}
// Change the build directory
// Don't need to reconfigure the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir("output")
// A task that generates a source file and writes the result to an output directory
abstract class GenerateSource extends DefaultTask {
// The configuration file to use to generate the source file
@InputFile
abstract RegularFileProperty getConfigFile()
// The directory to write source files to
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
def compile() {
def inFile = configFile.get().asFile
logger.quiet("configuration file = $inFile")
def dir = outputDir.get().asFile
logger.quiet("output dir = $dir")
def className = inFile.text.trim()
def srcFile = new File(dir, "${className}.java")
srcFile.text = "public class ${className} { ... }"
}
}
// Create the source generation task
tasks.register('generate', GenerateSource) {
// Configure the locations, relative to the project and build directories
configFile = layout.projectDirectory.file('src/config.txt')
outputDir = layout.buildDirectory.dir('generated-source')
}
// Change the build directory
// Don't need to reconfigure the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir('output')
$ gradle generate > Task :generate configuration file = /home/user/gradle/samples/src/config.txt output dir = /home/user/gradle/samples/output/generated-source BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
$ gradle generate > Task :generate configuration file = /home/user/gradle/samples/kotlin/src/config.txt output dir = /home/user/gradle/samples/kotlin/output/generated-source BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
此示例通过Project.getLayout()以及ProjectLayout.getBuildDirectory()和ProjectLayout.getProjectDirectory()创建表示项目中位置和构建目录的提供程序。
要关闭循环,请注意 aDirectoryProperty
或简单的Directory
可以转换为 a ,允许使用DirectoryProperty.getAsFileTree()或Directory.getAsFileTree()FileTree
查询目录中包含的文件和目录。从 a或 a ,您可以使用DirectoryProperty.files(Object...)或Directory.files(Object...)创建包含目录中包含的一组文件的实例。DirectoryProperty
Directory
FileCollection
使用任务输入和输出
许多构建都有多个连接在一起的任务,其中一个任务将另一个任务的输出用作输入。
为了实现这项工作,我们需要配置每个任务,以了解在哪里查找其输入以及在哪里放置其输出。确保生产任务和消费任务配置在相同的位置,并在任务之间附加任务依赖关系。如果这些值中的任何一个可由用户配置或由多个插件配置,则这可能会很麻烦且脆弱,因为任务属性需要以正确的顺序和位置进行配置,并且任务依赖项在值更改时保持同步。
APIProperty
通过跟踪属性的值和生成该值的任务使这变得更容易。
作为示例,请考虑以下带有连接在一起的生产者和消费者任务的插件:
abstract class Producer : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun produce() {
val message = "Hello, World!"
val output = outputFile.get().asFile
output.writeText( message)
logger.quiet("Wrote '${message}' to ${output}")
}
}
abstract class Consumer : DefaultTask() {
@get:InputFile
abstract val inputFile: RegularFileProperty
@TaskAction
fun consume() {
val input = inputFile.get().asFile
val message = input.readText()
logger.quiet("Read '${message}' from ${input}")
}
}
val producer = tasks.register<Producer>("producer")
val consumer = tasks.register<Consumer>("consumer")
consumer {
// Connect the producer task output to the consumer task input
// Don't need to add a task dependency to the consumer task. This is automatically added
inputFile = producer.flatMap { it.outputFile }
}
producer {
// Set values for the producer lazily
// Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
outputFile = layout.buildDirectory.file("file.txt")
}
// Change the build directory.
// Don't need to update producer.outputFile and consumer.inputFile. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir("output")
abstract class Producer extends DefaultTask {
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void produce() {
String message = 'Hello, World!'
def output = outputFile.get().asFile
output.text = message
logger.quiet("Wrote '${message}' to ${output}")
}
}
abstract class Consumer extends DefaultTask {
@InputFile
abstract RegularFileProperty getInputFile()
@TaskAction
void consume() {
def input = inputFile.get().asFile
def message = input.text
logger.quiet("Read '${message}' from ${input}")
}
}
def producer = tasks.register("producer", Producer)
def consumer = tasks.register("consumer", Consumer)
consumer.configure {
// Connect the producer task output to the consumer task input
// Don't need to add a task dependency to the consumer task. This is automatically added
inputFile = producer.flatMap { it.outputFile }
}
producer.configure {
// Set values for the producer lazily
// Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
outputFile = layout.buildDirectory.file('file.txt')
}
// Change the build directory.
// Don't need to update producer.outputFile and consumer.inputFile. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir('output')
$ gradle consumer > Task :producer Wrote 'Hello, World!' to /home/user/gradle/samples/output/file.txt > Task :consumer Read 'Hello, World!' from /home/user/gradle/samples/output/file.txt BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed
$ gradle consumer > Task :producer Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/output/file.txt > Task :consumer Read 'Hello, World!' from /home/user/gradle/samples/kotlin/output/file.txt BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed
在上面的示例中,任务输出和输入在定义任何位置之前就已连接。 setter 可以在任务执行之前随时调用,并且更改将自动影响所有相关的输入和输出属性。
此示例中需要注意的另一个重要事项是不存在任何显式任务依赖性。使用Providers
跟踪哪个任务产生其值来表示任务输出,并将它们用作任务输入将隐式添加正确的任务依赖项。
隐式任务依赖项也适用于非文件的输入属性:
abstract class Producer : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun produce() {
val message = "Hello, World!"
val output = outputFile.get().asFile
output.writeText( message)
logger.quiet("Wrote '${message}' to ${output}")
}
}
abstract class Consumer : DefaultTask() {
@get:Input
abstract val message: Property<String>
@TaskAction
fun consume() {
logger.quiet(message.get())
}
}
val producer = tasks.register<Producer>("producer") {
// Set values for the producer lazily
// Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
outputFile = layout.buildDirectory.file("file.txt")
}
tasks.register<Consumer>("consumer") {
// Connect the producer task output to the consumer task input
// Don't need to add a task dependency to the consumer task. This is automatically added
message = producer.flatMap { it.outputFile }.map { it.asFile.readText() }
}
abstract class Producer extends DefaultTask {
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void produce() {
String message = 'Hello, World!'
def output = outputFile.get().asFile
output.text = message
logger.quiet("Wrote '${message}' to ${output}")
}
}
abstract class Consumer extends DefaultTask {
@Input
abstract Property<String> getMessage()
@TaskAction
void consume() {
logger.quiet(message.get())
}
}
def producer = tasks.register('producer', Producer) {
// Set values for the producer lazily
// Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
outputFile = layout.buildDirectory.file('file.txt')
}
tasks.register('consumer', Consumer) {
// Connect the producer task output to the consumer task input
// Don't need to add a task dependency to the consumer task. This is automatically added
message = producer.flatMap { it.outputFile }.map { it.asFile.text }
}
$ gradle consumer > Task :producer Wrote 'Hello, World!' to /home/user/gradle/samples/build/file.txt > Task :consumer Hello, World! BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed
$ gradle consumer > Task :producer Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/build/file.txt > Task :consumer Hello, World! BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed
使用集合
Gradle 提供了两种惰性属性类型来帮助配置Collection
属性。
它们的工作方式与任何其他提供程序完全相同Provider
,并且就像文件提供程序一样,它们有额外的建模:
-
对于
List
值,该接口称为ListProperty。
您可以ListProperty
使用ObjectFactory.listProperty(Class)并指定元素类型来创建新的。 -
对于
Set
值,该接口称为SetProperty。
您可以SetProperty
使用ObjectFactory.setProperty(Class)并指定元素类型来创建新的。
这种类型的属性允许您使用HasMultipleValues.set(Iterable)和HasMultipleValues.set(Provider)覆盖整个集合值,或者通过各种add
方法添加新元素:
-
HasMultipleValues.add(T) : 将单个元素添加到集合中
-
HasMultipleValues.add(Provider):将延迟计算的元素添加到集合中
-
HasMultipleValues.addAll(Provider):将延迟计算的元素集合添加到列表中
就像 every 一样Provider
,集合是在调用Provider.get()时计算的。以下示例显示了ListProperty 的实际操作:
abstract class Producer : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun produce() {
val message = "Hello, World!"
val output = outputFile.get().asFile
output.writeText( message)
logger.quiet("Wrote '${message}' to ${output}")
}
}
abstract class Consumer : DefaultTask() {
@get:InputFiles
abstract val inputFiles: ListProperty<RegularFile>
@TaskAction
fun consume() {
inputFiles.get().forEach { inputFile ->
val input = inputFile.asFile
val message = input.readText()
logger.quiet("Read '${message}' from ${input}")
}
}
}
val producerOne = tasks.register<Producer>("producerOne")
val producerTwo = tasks.register<Producer>("producerTwo")
tasks.register<Consumer>("consumer") {
// Connect the producer task outputs to the consumer task input
// Don't need to add task dependencies to the consumer task. These are automatically added
inputFiles.add(producerOne.get().outputFile)
inputFiles.add(producerTwo.get().outputFile)
}
// Set values for the producer tasks lazily
// Don't need to update the consumer.inputFiles property. This is automatically updated as producer.outputFile changes
producerOne { outputFile = layout.buildDirectory.file("one.txt") }
producerTwo { outputFile = layout.buildDirectory.file("two.txt") }
// Change the build directory.
// Don't need to update the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir("output")
abstract class Producer extends DefaultTask {
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void produce() {
String message = 'Hello, World!'
def output = outputFile.get().asFile
output.text = message
logger.quiet("Wrote '${message}' to ${output}")
}
}
abstract class Consumer extends DefaultTask {
@InputFiles
abstract ListProperty<RegularFile> getInputFiles()
@TaskAction
void consume() {
inputFiles.get().each { inputFile ->
def input = inputFile.asFile
def message = input.text
logger.quiet("Read '${message}' from ${input}")
}
}
}
def producerOne = tasks.register('producerOne', Producer)
def producerTwo = tasks.register('producerTwo', Producer)
tasks.register('consumer', Consumer) {
// Connect the producer task outputs to the consumer task input
// Don't need to add task dependencies to the consumer task. These are automatically added
inputFiles.add(producerOne.get().outputFile)
inputFiles.add(producerTwo.get().outputFile)
}
// Set values for the producer tasks lazily
// Don't need to update the consumer.inputFiles property. This is automatically updated as producer.outputFile changes
producerOne.configure { outputFile = layout.buildDirectory.file('one.txt') }
producerTwo.configure { outputFile = layout.buildDirectory.file('two.txt') }
// Change the build directory.
// Don't need to update the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir('output')
$ gradle consumer > Task :producerOne Wrote 'Hello, World!' to /home/user/gradle/samples/output/one.txt > Task :producerTwo Wrote 'Hello, World!' to /home/user/gradle/samples/output/two.txt > Task :consumer Read 'Hello, World!' from /home/user/gradle/samples/output/one.txt Read 'Hello, World!' from /home/user/gradle/samples/output/two.txt BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed
$ gradle consumer > Task :producerOne Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/output/one.txt > Task :producerTwo Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/output/two.txt > Task :consumer Read 'Hello, World!' from /home/user/gradle/samples/kotlin/output/one.txt Read 'Hello, World!' from /home/user/gradle/samples/kotlin/output/two.txt BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed
使用地图
Gradle 提供了一个惰性MapProperty类型来允许Map
配置值。您可以MapProperty
使用ObjectFactory.mapProperty(Class, Class)创建实例。
与其他属性类型类似,aMapProperty
有一个set()方法,可用于指定属性的值。一些附加方法允许将具有惰性值的条目添加到映射中。
abstract class Generator: DefaultTask() {
@get:Input
abstract val properties: MapProperty<String, Int>
@TaskAction
fun generate() {
properties.get().forEach { entry ->
logger.quiet("${entry.key} = ${entry.value}")
}
}
}
// Some values to be configured later
var b = 0
var c = 0
tasks.register<Generator>("generate") {
properties.put("a", 1)
// Values have not been configured yet
properties.put("b", providers.provider { b })
properties.putAll(providers.provider { mapOf("c" to c, "d" to c + 1) })
}
// Configure the values. There is no need to reconfigure the task
b = 2
c = 3
abstract class Generator extends DefaultTask {
@Input
abstract MapProperty<String, Integer> getProperties()
@TaskAction
void generate() {
properties.get().each { key, value ->
logger.quiet("${key} = ${value}")
}
}
}
// Some values to be configured later
def b = 0
def c = 0
tasks.register('generate', Generator) {
properties.put("a", 1)
// Values have not been configured yet
properties.put("b", providers.provider { b })
properties.putAll(providers.provider { [c: c, d: c + 1] })
}
// Configure the values. There is no need to reconfigure the task
b = 2
c = 3
$ gradle generate > Task :generate a = 1 b = 2 c = 3 d = 4 BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
将约定应用于属性
通常,您希望对要在未配置值的情况下使用的属性应用一些约定或默认值。您可以使用该convention()
方法来实现此目的。此方法接受一个值或 a Provider
,并且这将用作该值,直到配置其他值。
tasks.register("show") {
val property = objects.property(String::class)
// Set a convention
property.convention("convention 1")
println("value = " + property.get())
// Can replace the convention
property.convention("convention 2")
println("value = " + property.get())
property.set("explicit value")
// Once a value is set, the convention is ignored
property.convention("ignored convention")
doLast {
println("value = " + property.get())
}
}
tasks.register("show") {
def property = objects.property(String)
// Set a convention
property.convention("convention 1")
println("value = " + property.get())
// Can replace the convention
property.convention("convention 2")
println("value = " + property.get())
property.set("explicit value")
// Once a value is set, the convention is ignored
property.convention("ignored convention")
doLast {
println("value = " + property.get())
}
}
$ gradle show value = convention 1 value = convention 2 > Task :show value = explicit value BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
使属性不可修改
任务或项目的大多数属性旨在通过插件或构建脚本进行配置,以便它们可以为该构建使用特定值。
例如,指定编译任务的输出目录的属性可能以插件指定的值开头。然后构建脚本可能会将该值更改为某个自定义位置,然后任务运行时将使用该值。但是,一旦任务开始运行,我们希望防止进一步的属性更改。通过这种方式,我们可以避免因不同的使用者(例如任务操作、Gradle 的最新检查、构建缓存或其他任务)使用不同的属性值而导致的错误。
惰性属性提供了多种方法,您可以使用这些方法在配置值后禁止更改其值。 FinalizeValue ()方法计算属性的最终值并防止对该属性进行进一步更改。
libVersioning.version.finalizeValue()
当属性的值来自 a 时Provider
,将向提供者查询其当前值,结果将成为该属性的最终值。此最终值取代了提供者,并且该属性不再跟踪提供者的值。调用此方法还会使属性实例不可修改,并且任何进一步更改该属性值的尝试都将失败。当任务开始执行时,Gradle 会自动将任务的属性设为最终属性。
FinalizeValueOnRead()方法类似,只不过在查询属性值之前不会计算属性的最终值。
modifiedFiles.finalizeValueOnRead()
换句话说,该方法根据需要延迟计算最终值,而finalizeValue()
急切地计算最终值。当该值的计算成本可能很高或尚未配置时,可以使用此方法。您还希望确保该属性的所有使用者在查询该值时看到相同的值。
使用提供商 API
成功使用 Provider API 的指南:
-
Property和Provider类型具有查询或配置值所需的所有重载。因此,您应该遵循以下准则:
-
对于可配置属性,直接通过单个 getter公开属性。
-
对于不可配置的属性,直接通过单个 getter公开Provider 。
-
-
避免通过引入额外的 getter 和 setter 来简化代码中的调用,例如
obj.getProperty().get()
和。obj.getProperty().set(T)
-
将插件迁移到使用提供程序时,请遵循以下准则:
惰性对象 API 参考
将这些类型用于只读值:
- 提供者<T>
-
其值为以下实例的属性
T
- 工厂
-
-
ProviderFactory.provider(Callable)。始终优先选择其他工厂方法之一而不是此方法。
将这些类型用于可变值:
- 属性<T>
-
其值为以下实例的属性
T
开发并行任务
Gradle 提供了一个 API,可以将任务分割成可以并行执行的部分。
这使得 Gradle 能够充分利用可用资源并更快地完成构建。
工人API
Worker API 提供了将任务操作的执行分解为离散的工作单元,然后并发和异步执行该工作的能力。
工作线程 API 示例
了解如何使用 API 的最佳方法是完成将现有自定义任务转换为使用 Worker API 的过程:
-
您将首先创建一个自定义任务类,为一组可配置的文件生成 MD5 哈希值。
-
然后,您将转换此自定义任务以使用 Worker API。
-
然后,我们将探索以不同的隔离级别运行任务。
在此过程中,您将了解 Worker API 的基础知识及其提供的功能。
步骤 1. 创建自定义任务类
首先,创建一个自定义任务,生成一组可配置文件的 MD5 哈希值。
在新目录中,创建一个buildSrc/build.gradle(.kts)
文件:
repositories {
mavenCentral()
}
dependencies {
implementation("commons-io:commons-io:2.5")
implementation("commons-codec:commons-codec:1.9") // (1)
}
repositories {
mavenCentral()
}
dependencies {
implementation 'commons-io:commons-io:2.5'
implementation 'commons-codec:commons-codec:1.9' // (1)
}
-
您的自定义任务类将使用Apache Commons Codec生成 MD5 哈希值。
接下来,在您的目录中创建一个自定义任务类buildSrc/src/main/java
。你应该命名这个类CreateMD5
:
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkerExecutor;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
abstract public class CreateMD5 extends SourceTask { // (1)
@OutputDirectory
abstract public DirectoryProperty getDestinationDirectory(); // (2)
@TaskAction
public void createHashes() {
for (File sourceFile : getSource().getFiles()) { // (3)
try {
InputStream stream = new FileInputStream(sourceFile);
System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
// Artificially make this task slower.
Thread.sleep(3000); // (4)
Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5"); // (5)
FileUtils.writeStringToFile(md5File.get().getAsFile(), DigestUtils.md5Hex(stream), (String) null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
-
SourceTask是对一组源文件进行操作的任务的便捷类型。
-
任务输出将进入配置的目录。
-
该任务迭代所有定义为“源文件”的文件并创建每个文件的 MD5 哈希值。
-
插入人工睡眠来模拟对大文件进行哈希处理(示例文件不会那么大)。
-
每个文件的 MD5 哈希值将写入输出目录中,并写入具有“md5”扩展名的同名文件中。
接下来,创建一个build.gradle(.kts)
注册新CreateMD5
任务的任务:
plugins { id("base") } // (1)
tasks.register<CreateMD5>("md5") {
destinationDirectory = project.layout.buildDirectory.dir("md5") // (2)
source(project.layout.projectDirectory.file("src")) // (3)
}
plugins { id 'base' } // (1)
tasks.register("md5", CreateMD5) {
destinationDirectory = project.layout.buildDirectory.dir("md5") // (2)
source(project.layout.projectDirectory.file('src')) // (3)
}
-
应用该
base
插件,以便您可以clean
执行删除输出的任务。 -
MD5 哈希文件将被写入
build/md5
. -
此任务将为
src
目录中的每个文件生成 MD5 哈希文件。
您将需要一些源来生成 MD5 哈希值。在目录中创建三个文件src
:
Intellectual growth should commence at birth and cease only at death.
I was born not knowing and have had only a little time to change that here and there.
Intelligence is the ability to adapt to change.
此时,您可以通过运行来测试您的任务./gradlew md5
:
$ gradle md5
输出应类似于:
> Task :md5 Generating MD5 for einstein.txt... Generating MD5 for feynman.txt... Generating MD5 for hawking.txt... BUILD SUCCESSFUL in 9s 3 actionable tasks: 3 executed
在该build/md5
目录中,您现在应该看到相应的文件,其md5
扩展名包含该目录中文件的 MD5 哈希值src
。请注意,该任务至少需要 9 秒才能运行,因为它一次对每个文件进行哈希处理(即,每个文件约 3 秒对三个文件进行哈希处理)。
步骤 2. 转换为 Worker API
尽管此任务按顺序处理每个文件,但每个文件的处理独立于任何其他文件。这项工作可以并行完成并利用多个处理器。这就是 Worker API 可以提供帮助的地方。
要使用 Worker API,您需要定义一个接口来表示每个工作单元的参数并扩展org.gradle.workers.WorkParameters
。
为了生成 MD5 哈希文件,工作单元需要两个参数:
-
要散列的文件,
-
要写入哈希值的文件。
不需要创建具体的实现,因为 Gradle 会在运行时为我们生成一个。
import org.gradle.api.file.RegularFileProperty;
import org.gradle.workers.WorkParameters;
public interface MD5WorkParameters extends WorkParameters {
RegularFileProperty getSourceFile(); // (1)
RegularFileProperty getMD5File();
}
-
使用
Property
对象来表示源文件和 MD5 哈希文件。
然后,您需要将自定义任务中为每个单独文件执行工作的部分重构为单独的类。这个类是您的“工作单元”实现,它应该是一个扩展的抽象类org.gradle.workers.WorkAction
:
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.gradle.workers.WorkAction;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public abstract class GenerateMD5 implements WorkAction<MD5WorkParameters> { // (1)
@Override
public void execute() {
try {
File sourceFile = getParameters().getSourceFile().getAsFile().get();
File md5File = getParameters().getMD5File().getAsFile().get();
InputStream stream = new FileInputStream(sourceFile);
System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
// Artificially make this task slower.
Thread.sleep(3000);
FileUtils.writeStringToFile(md5File, DigestUtils.md5Hex(stream), (String) null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
-
不要实现该
getParameters()
方法 - Gradle 将在运行时注入该方法。
现在,更改您的自定义任务类以将工作提交到 WorkerExecutor,而不是自行执行工作。
import org.gradle.api.Action;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.workers.*;
import org.gradle.api.file.DirectoryProperty;
import javax.inject.Inject;
import java.io.File;
abstract public class CreateMD5 extends SourceTask {
@OutputDirectory
abstract public DirectoryProperty getDestinationDirectory();
@Inject
abstract public WorkerExecutor getWorkerExecutor(); // (1)
@TaskAction
public void createHashes() {
WorkQueue workQueue = getWorkerExecutor().noIsolation(); // (2)
for (File sourceFile : getSource().getFiles()) {
Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
workQueue.submit(GenerateMD5.class, parameters -> { // (3)
parameters.getSourceFile().set(sourceFile);
parameters.getMD5File().set(md5File);
});
}
}
}
-
需要WorkerExecutor服务才能提交您的工作。创建一个带注释的抽象 getter 方法
javax.inject.Inject
,Gradle 将在创建任务时在运行时注入服务。 -
在提交工作之前,获取
WorkQueue
具有所需隔离模式的对象(如下所述)。 -
提交工作单元时,指定工作单元实现(在本例中为 )
GenerateMD5
,并配置其参数。
此时,您应该能够重新运行您的任务:
$ gradle clean md5 > Task :md5 Generating MD5 for einstein.txt... Generating MD5 for feynman.txt... Generating MD5 for hawking.txt... BUILD SUCCESSFUL in 3s 3 actionable tasks: 3 executed
结果看起来应该与以前相同,但由于工作单元是并行执行的,MD5 哈希文件可能会以不同的顺序生成。然而,这一次任务运行得更快了。这是因为 Worker API 并行而不是顺序地对每个文件执行 MD5 计算。
步骤 3. 更改隔离模式
隔离模式控制 Gradle 将工作项彼此以及 Gradle 运行时的其余部分隔离的程度。
可以通过三种方法来WorkerExecutor
控制:
-
noIsolation()
-
classLoaderIsolation()
-
processIsolation()
该noIsolation()
模式是最低级别的隔离,将防止工作单元更改项目状态。这是最快的隔离模式,因为它需要最少的开销来设置和执行工作项。但是,它将使用单个共享类加载器来完成所有工作单元。这意味着每个工作单元都可以通过静态类状态相互影响。这也意味着每个工作单元在 buildscript 类路径上使用相同版本的库。如果您希望用户能够将任务配置为使用不同(但兼容)版本的
Apache Commons Codec库运行,则需要使用不同的隔离模式。
首先,您必须将依赖项更改为buildSrc/build.gradle
be compileOnly
。这告诉 Gradle 在构建类时应该使用此依赖项,但不应将其放在构建脚本类路径中:
repositories {
mavenCentral()
}
dependencies {
implementation("commons-io:commons-io:2.5")
compileOnly("commons-codec:commons-codec:1.9")
}
repositories {
mavenCentral()
}
dependencies {
implementation 'commons-io:commons-io:2.5'
compileOnly 'commons-codec:commons-codec:1.9'
}
接下来,更改CreateMD5
任务以允许用户配置他们想要使用的编解码器库的版本。它将在运行时解析库的适当版本并配置工作人员以使用该版本。
该classLoaderIsolation()
方法告诉 Gradle 在具有隔离类加载器的线程中运行此工作:
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.process.JavaForkOptions;
import org.gradle.workers.*;
import javax.inject.Inject;
import java.io.File;
import java.util.Set;
abstract public class CreateMD5 extends SourceTask {
@InputFiles
abstract public ConfigurableFileCollection getCodecClasspath(); // (1)
@OutputDirectory
abstract public DirectoryProperty getDestinationDirectory();
@Inject
abstract public WorkerExecutor getWorkerExecutor();
@TaskAction
public void createHashes() {
WorkQueue workQueue = getWorkerExecutor().classLoaderIsolation(workerSpec -> {
workerSpec.getClasspath().from(getCodecClasspath()); // (2)
});
for (File sourceFile : getSource().getFiles()) {
Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
workQueue.submit(GenerateMD5.class, parameters -> {
parameters.getSourceFile().set(sourceFile);
parameters.getMD5File().set(md5File);
});
}
}
}
-
公开编解码器库类路径的输入属性。
-
创建工作队列时在ClassLoaderWorkerSpec上配置类路径。
接下来,您需要配置构建,以便它有一个存储库来在任务执行时查找编解码器版本。我们还创建一个依赖项来解析此存储库中的编解码器库:
plugins { id("base") }
repositories {
mavenCentral() // (1)
}
val codec = configurations.create("codec") { // (2)
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}
isVisible = false
isCanBeConsumed = false
}
dependencies {
codec("commons-codec:commons-codec:1.10") // (3)
}
tasks.register<CreateMD5>("md5") {
codecClasspath.from(codec) // (4)
destinationDirectory = project.layout.buildDirectory.dir("md5")
source(project.layout.projectDirectory.file("src"))
}
plugins { id 'base' }
repositories {
mavenCentral() // (1)
}
configurations.create('codec') { // (2)
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
}
visible = false
canBeConsumed = false
}
dependencies {
codec 'commons-codec:commons-codec:1.10' // (3)
}
tasks.register('md5', CreateMD5) {
codecClasspath.from(configurations.codec) // (4)
destinationDirectory = project.layout.buildDirectory.dir('md5')
source(project.layout.projectDirectory.file('src'))
}
-
添加一个存储库来解析编解码器库 - 这可以是与用于构建
CreateMD5
任务类的存储库不同的存储库。 -
添加配置来解析我们的编解码器库版本。
-
配置Apache Commons Codec的备用兼容版本。
-
配置
md5
任务以使用配置作为其类路径。请注意,直到任务执行后,配置才会被解析。
现在,如果您运行任务,它应该使用编解码器库的配置版本按预期工作:
$ gradle clean md5 > Task :md5 Generating MD5 for einstein.txt... Generating MD5 for feynman.txt... Generating MD5 for hawking.txt... BUILD SUCCESSFUL in 3s 3 actionable tasks: 3 executed
步骤 4. 创建一个工作守护进程
有时,在执行工作项时需要利用更高级别的隔离。例如,外部库可能依赖于要设置的某些系统属性,这可能会在工作项之间发生冲突。或者,库可能与 Gradle 运行的 JDK 版本不兼容,并且可能需要使用不同的版本运行。
Worker API 可以使用processIsolation()
使工作在单独的“worker 守护进程”中执行的方法来适应这种情况。这些工作守护进程将在构建过程中持续存在,并且可以在后续构建过程中重用。但是,如果系统资源不足,Gradle 将停止未使用的工作守护进程。
要使用工作守护进程,请processIsolation()
在创建WorkQueue
.您可能还想为新流程配置自定义设置:
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.process.JavaForkOptions;
import org.gradle.workers.*;
import javax.inject.Inject;
import java.io.File;
import java.util.Set;
abstract public class CreateMD5 extends SourceTask {
@InputFiles
abstract public ConfigurableFileCollection getCodecClasspath(); // (1)
@OutputDirectory
abstract public DirectoryProperty getDestinationDirectory();
@Inject
abstract public WorkerExecutor getWorkerExecutor();
@TaskAction
public void createHashes() {
// (1)
WorkQueue workQueue = getWorkerExecutor().processIsolation(workerSpec -> {
workerSpec.getClasspath().from(getCodecClasspath());
workerSpec.forkOptions(options -> {
options.setMaxHeapSize("64m"); // (2)
});
});
for (File sourceFile : getSource().getFiles()) {
Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
workQueue.submit(GenerateMD5.class, parameters -> {
parameters.getSourceFile().set(sourceFile);
parameters.getMD5File().set(md5File);
});
}
}
}
-
将隔离模式更改为
PROCESS
。 -
为新进程设置JavaForkOptions 。
现在,您应该能够运行您的任务,并且它将按预期工作,但使用工作守护进程:
$ gradle clean md5 > Task :md5 Generating MD5 for einstein.txt... Generating MD5 for feynman.txt... Generating MD5 for hawking.txt... BUILD SUCCESSFUL in 3s 3 actionable tasks: 3 executed
请注意,执行时间可能会很长。这是因为 Gradle 必须为每个工作守护进程启动一个新进程,这是昂贵的。
但是,如果您第二次运行任务,您会发现它运行得更快。这是因为在初始构建期间启动的工作守护进程已持续存在,并且可以在后续构建期间立即使用:
$ gradle clean md5 > Task :md5 Generating MD5 for einstein.txt... Generating MD5 for feynman.txt... Generating MD5 for hawking.txt... BUILD SUCCESSFUL in 1s 3 actionable tasks: 3 executed
隔离模式
Gradle 提供了三种隔离模式,可以在创建WorkQueue时进行配置,并在WorkerExecutor上使用以下方法之一指定:
WorkerExecutor.noIsolation()
-
这表明工作应该在具有最小隔离的线程中运行。
例如,它将共享与任务加载相同的类加载器。这是最快的隔离级别。 WorkerExecutor.classLoaderIsolation()
-
这表明工作应该在具有隔离类加载器的线程中运行。
类加载器将具有来自加载工作单元实现类的类加载器的类路径以及通过添加的任何其他类路径条目ClassLoaderWorkerSpec.getClasspath()
。 WorkerExecutor.processIsolation()
-
这表明工作应该通过在单独的进程中执行工作来以最大隔离级别运行。
该进程的类加载器将使用加载工作单元的类加载器中的类路径以及通过添加的任何其他类路径条目ClassLoaderWorkerSpec.getClasspath()
。此外,该进程将是一个工作守护进程,它将保持活动状态,并且可以重用于具有相同要求的未来工作项。可以使用ProcessWorkerSpec.forkOptions(org.gradle.api.Action)使用与 Gradle JVM 不同的设置来配置此进程。
工人守护进程
使用 时processIsolation()
,Gradle 将启动一个长期存在的工作守护进程,可以在未来的工作项中重用。
// Create a WorkQueue with process isolation
val workQueue = workerExecutor.processIsolation() {
// Configure the options for the forked process
forkOptions {
maxHeapSize = "512m"
systemProperty("org.gradle.sample.showFileSize", "true")
}
}
// Create and submit a unit of work for each file
source.forEach { file ->
workQueue.submit(ReverseFile::class) {
fileToReverse = file
destinationDir = outputDir
}
}
// Create a WorkQueue with process isolation
WorkQueue workQueue = workerExecutor.processIsolation() { ProcessWorkerSpec spec ->
// Configure the options for the forked process
forkOptions { JavaForkOptions options ->
options.maxHeapSize = "512m"
options.systemProperty "org.gradle.sample.showFileSize", "true"
}
}
// Create and submit a unit of work for each file
source.each { file ->
workQueue.submit(ReverseFile.class) { ReverseParameters parameters ->
parameters.fileToReverse = file
parameters.destinationDir = outputDir
}
}
当提交工作守护进程的工作单元时,Gradle 将首先查看是否已存在兼容的空闲守护进程。如果是,它会将工作单元发送到空闲守护进程,将其标记为忙碌。如果没有,它将启动一个新的守护进程。在评估兼容性时,Gradle 会考虑许多标准,所有这些标准都可以通过ProcessWorkerSpec.forkOptions(org.gradle.api.Action)进行控制。
默认情况下,工作守护进程以 512MB 的最大堆启动。这可以通过调整工人的分叉选项来改变。
- 可执行文件
-
仅当守护程序使用相同的 Java 可执行文件时,该守护程序才被视为兼容。
- 类路径
-
如果守护进程的类路径包含所请求的所有类路径条目,则该守护进程被认为是兼容的。
请注意,仅当类路径与请求的类路径完全匹配时,守护程序才被视为兼容。 - 堆设置
-
如果守护进程至少具有与请求相同的堆大小设置,则该守护进程被认为是兼容的。
换句话说,具有比请求更高的堆设置的守护进程将被视为兼容。 - jvm 参数
-
如果守护进程已设置了请求的所有 JVM 参数,则该守护进程是兼容的。
请注意,如果守护程序具有超出请求的其他 JVM 参数(除了那些特别处理的参数,例如堆设置、断言、调试等),则该守护程序是兼容的。 - 系统属性
-
如果守护程序已将请求的所有系统属性设置为相同的值,则该守护程序被视为兼容。
请注意,如果守护程序具有超出所要求的其他系统属性,则该守护程序是兼容的。 - 环境变量
-
如果守护进程已将请求的所有环境变量设置为相同的值,则该守护进程被认为是兼容的。
请注意,如果守护程序的环境变量多于请求的数量,则该守护程序是兼容的。 - 引导类路径
-
如果守护进程包含所请求的所有引导类路径条目,则该守护进程被认为是兼容的。
请注意,如果守护程序的引导类路径条目多于请求的数量,则该守护程序是兼容的。 - 调试
-
仅当 debug 设置为与请求的值相同(
true
或false
)时,守护程序才被视为兼容。 - 启用断言
-
仅当启用断言设置为与请求的值相同(
true
或false
)时,守护程序才被视为兼容。 - 默认字符编码
-
仅当默认字符编码设置为与请求的值相同时,守护程序才被视为兼容。
工作守护进程将保持运行,直到启动它们的构建守护进程停止或系统内存变得稀缺。当系统内存不足时,Gradle 将停止工作守护进程以最大程度地减少内存消耗。
笔记
|
有关将普通任务操作转换为使用辅助 API 的分步说明可以在开发并行任务部分中找到。 |
取消和超时
为了支持取消(例如,当用户使用 CTRL+C 停止构建时)和任务超时,自定义任务应该对中断其执行线程做出反应。对于通过工作 API 提交的工作项也是如此。如果任务在 10 秒内没有响应中断,守护进程将关闭以释放系统资源。
高级任务
增量任务
UP-TO-DATE
在 Gradle 中,由于增量构建功能,实现在输入和输出已经存在时跳过执行的任务既简单又高效。
然而,有时自上次执行以来只有少数输入文件发生了变化,最好避免重新处理所有未更改的输入。这种情况在一对一地将输入文件转换为输出文件的任务中很常见。
要优化构建过程,您可以使用增量任务。这种方法可确保仅处理过时的输入文件,从而提高构建性能。
实施增量任务
对于增量处理输入的任务,该任务必须包含增量任务操作。
这是一个具有单个InputChanges参数的任务操作方法。该参数告诉 Gradle 该操作只想处理更改的输入。
此外,该任务需要使用@Incremental
或声明至少一个增量文件输入属性@SkipWhenEmpty
:
public class IncrementalReverseTask : DefaultTask() {
@get:Incremental
@get:InputDirectory
val inputDir: DirectoryProperty = project.objects.directoryProperty()
@get:OutputDirectory
val outputDir: DirectoryProperty = project.objects.directoryProperty()
@get:Input
val inputProperty: RegularFileProperty = project.objects.fileProperty() // File input property
@TaskAction
fun execute(inputs: InputChanges) { // InputChanges parameter
val msg = if (inputs.isIncremental) "CHANGED inputs are out of date"
else "ALL inputs are out of date"
println(msg)
}
}
class IncrementalReverseTask extends DefaultTask {
@Incremental
@InputDirectory
def File inputDir
@OutputDirectory
def File outputDir
@Input
def inputProperty // File input property
@TaskAction
void execute(InputChanges inputs) { // InputChanges parameter
println inputs.incremental ? "CHANGED inputs are out of date"
: "ALL inputs are out of date"
}
}
重要的
|
要查询输入文件属性的增量更改,该属性必须始终返回相同的实例。实现此目的的最简单方法是使用以下属性类型之一: 您可以在延迟配置中了解有关 |
增量任务操作可用于InputChanges.getFileChanges()
找出给定的基于文件的输入属性(类型为或 )RegularFileProperty
的文件已更改。DirectoryProperty
ConfigurableFileCollection
该方法返回FileChangesIterable
类型,进而可以查询以下内容:
以下示例演示了具有目录输入的增量任务。它假设该目录包含文本文件的集合,并将它们复制到输出目录,反转每个文件中的文本:
abstract class IncrementalReverseTask : DefaultTask() {
@get:Incremental
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputDirectory
abstract val inputDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
abstract val inputProperty: Property<String>
@TaskAction
fun execute(inputChanges: InputChanges) {
println(
if (inputChanges.isIncremental) "Executing incrementally"
else "Executing non-incrementally"
)
inputChanges.getFileChanges(inputDir).forEach { change ->
if (change.fileType == FileType.DIRECTORY) return@forEach
println("${change.changeType}: ${change.normalizedPath}")
val targetFile = outputDir.file(change.normalizedPath).get().asFile
if (change.changeType == ChangeType.REMOVED) {
targetFile.delete()
} else {
targetFile.writeText(change.file.readText().reversed())
}
}
}
}
abstract class IncrementalReverseTask extends DefaultTask {
@Incremental
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputDirectory
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@Input
abstract Property<String> getInputProperty()
@TaskAction
void execute(InputChanges inputChanges) {
println(inputChanges.incremental
? 'Executing incrementally'
: 'Executing non-incrementally'
)
inputChanges.getFileChanges(inputDir).each { change ->
if (change.fileType == FileType.DIRECTORY) return
println "${change.changeType}: ${change.normalizedPath}"
def targetFile = outputDir.file(change.normalizedPath).get().asFile
if (change.changeType == ChangeType.REMOVED) {
targetFile.delete()
} else {
targetFile.text = change.file.text.reverse()
}
}
}
}
笔记
|
属性的类型inputDir 、其注释以及execute() 用于getFileChanges() 处理自上次构建以来已更改的文件子集的操作。如果相应的输入文件已被删除,则该操作将删除目标文件。
|
如果由于某种原因,任务以非增量方式执行(--rerun-tasks
例如,通过使用 运行),则所有文件都将报告为ADDED
,无论之前的状态如何。在这种情况下,Gradle 会自动删除之前的输出,因此增量任务必须只处理给定的文件。
对于像上面的示例这样的简单转换器任务,任务操作必须为任何过时的输入生成输出文件,并为任何已删除的输入删除输出文件。
重要的
|
一个任务可能只包含一个增量任务操作。 |
哪些输入被认为是过时的?
当之前执行过某个任务,并且执行后唯一的更改是增量输入文件属性时,Gradle 可以智能地确定需要处理哪些输入文件,这一概念称为增量执行。
在这种情况下,类InputChanges.getFileChanges()
中可用的方法org.gradle.work.InputChanges
提供与给定属性关联的所有输入文件的详细信息,这些输入文件已被ADDED
、REMOVED
或MODIFIED
。
然而,很多情况下Gradle无法确定哪些输入文件需要处理(即非增量执行)。示例包括:
-
没有先前执行的可用历史记录。
-
您正在使用不同版本的 Gradle 进行构建。目前,Gradle 不使用不同版本的任务历史记录。
-
upToDateWhen
添加到任务中的条件返回false
。 -
自上次执行以来,输入属性已更改。
-
自上次执行以来,非增量输入文件属性已更改。
-
自上次执行以来,一个或多个输出文件已更改。
在这些情况下,Gradle 会将所有输入文件报告为ADDED
,并且该getFileChanges()
方法将返回构成给定输入属性的所有文件的详细信息。
您可以使用该方法检查任务执行是否是增量的InputChanges.isIncremental()
。
正在执行的增量任务
IncrementalReverseTask
考虑第一次针对一组输入执行的实例。
在这种情况下,将考虑所有输入ADDED
,如下所示:
tasks.register<IncrementalReverseTask>("incrementalReverse") {
inputDir = file("inputs")
outputDir = layout.buildDirectory.dir("outputs")
inputProperty = project.findProperty("taskInputProperty") as String? ?: "original"
}
tasks.register('incrementalReverse', IncrementalReverseTask) {
inputDir = file('inputs')
outputDir = layout.buildDirectory.dir("outputs")
inputProperty = project.properties['taskInputProperty'] ?: 'original'
}
构建布局:
.
├── build.gradle
└── inputs
├── 1.txt
├── 2.txt
└── 3.txt
$ gradle -q incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt
当然,当任务没有任何改变地再次执行时,那么整个任务就是UP-TO-DATE
,并且任务动作不会被执行:
$ gradle incrementalReverse
> Task :incrementalReverse UP-TO-DATE
BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date
当以某种方式修改输入文件或添加新的输入文件时,重新执行任务会导致InputChanges.getFileChanges()
.
以下示例在运行增量任务之前修改一个文件的内容并添加另一个文件:
tasks.register("updateInputs") {
val inputsDir = layout.projectDirectory.dir("inputs")
outputs.dir(inputsDir)
doLast {
inputsDir.file("1.txt").asFile.writeText("Changed content for existing file 1.")
inputsDir.file("4.txt").asFile.writeText("Content for new file 4.")
}
}
tasks.register('updateInputs') {
def inputsDir = layout.projectDirectory.dir('inputs')
outputs.dir(inputsDir)
doLast {
inputsDir.file('1.txt').asFile.text = 'Changed content for existing file 1.'
inputsDir.file('4.txt').asFile.text = 'Content for new file 4.'
}
}
$ gradle -q updateInputs incrementalReverse Executing incrementally MODIFIED: 1.txt ADDED: 4.txt
笔记
|
各种突变任务(updateInputs 、removeInput 等)仅用于演示增量任务的行为。它们不应被视为您应该在自己的构建脚本中拥有的任务类型或任务实现。
|
InputChanges.getFileChanges()
当删除现有输入文件时,重新执行任务会导致as返回该文件REMOVED
。
以下示例在执行增量任务之前删除现有文件之一:
tasks.register<Delete>("removeInput") {
delete("inputs/3.txt")
}
tasks.register('removeInput', Delete) {
delete 'inputs/3.txt'
}
$ gradle -q removeInput incrementalReverse Executing incrementally REMOVED: 3.txt
当删除(或修改)输出文件时,Gradle 无法确定哪些输入文件已过期。在这种情况下,给定属性的所有输入文件的详细信息都由InputChanges.getFileChanges()
.
以下示例从构建目录中删除输出文件之一。但是,所有输入文件都被视为ADDED
:
tasks.register<Delete>("removeOutput") {
delete(layout.buildDirectory.file("outputs/1.txt"))
}
tasks.register('removeOutput', Delete) {
delete layout.buildDirectory.file("outputs/1.txt")
}
$ gradle -q removeOutput incrementalReverse Executing non-incrementally ADDED: 1.txt ADDED: 2.txt ADDED: 3.txt
我们要介绍的最后一个场景涉及修改非基于文件的输入属性时会发生什么。在这种情况下,Gradle 无法确定该属性如何影响任务输出,因此任务以非增量方式执行。这意味着给定属性的所有InputChanges.getFileChanges()
输入文件都由 返回,并且它们都被视为ADDED
.
以下示例taskInputProperty
在运行任务时将项目属性设置为新值incrementalReverse
。该项目属性用于初始化任务的属性,如本节第一个示例inputProperty
中所示。
这是本例中的预期输出:
$ gradle -q -PtaskInputProperty=changed incrementalReverse Executing non-incrementally ADDED: 1.txt ADDED: 2.txt ADDED: 3.txt
命令行选项
有时,用户希望在命令行而不是构建脚本上声明公开任务属性的值。如果属性值更改更频繁,则在命令行上传递属性值特别有用。
任务API支持标记属性的机制,以在运行时自动生成具有特定名称的相应命令行参数。
步骤 1. 声明命令行选项
要公开任务属性的新命令行选项,请使用Option注释属性的相应 setter 方法:
@Option(option = "flag", description = "Sets the flag")
选项需要强制标识符。您可以提供可选的描述。
任务可以公开与类中可用属性一样多的命令行选项。
选项也可以在任务类的超级接口中声明。如果多个接口声明相同的属性但具有不同的选项标志,则它们都将用于设置该属性。
在下面的示例中,自定义任务UrlVerify
通过进行 HTTP 调用并检查响应代码来验证是否可以解析 URL。要验证的 URL 可以通过 属性进行配置url
。该属性的 setter 方法用@Option注释:
import org.gradle.api.tasks.options.Option;
public class UrlVerify extends DefaultTask {
private String url;
@Option(option = "url", description = "Configures the URL to be verified.")
public void setUrl(String url) {
this.url = url;
}
@Input
public String getUrl() {
return url;
}
@TaskAction
public void verify() {
getLogger().quiet("Verifying URL '{}'", url);
// verify URL by making a HTTP call
}
}
为任务声明的所有选项都可以通过运行任务和选项呈现为控制台输出。help
--task
步骤 2. 在命令行上使用选项
命令行上的选项有一些规则:
-
该选项使用双破折号作为前缀,例如
--url
。单个破折号不符合任务选项的有效语法。 -
选项参数直接跟在任务声明之后,例如
verifyUrl --url=http://www.google.com/
。 -
可以在命令行上紧跟任务名称的任意顺序声明多个任务选项。
在前面的示例的基础上,构建脚本创建一个类型的任务实例UrlVerify
,并通过公开的选项从命令行提供一个值:
tasks.register<UrlVerify>("verifyUrl")
tasks.register('verifyUrl', UrlVerify)
$ gradle -q verifyUrl --url=http://www.google.com/ Verifying URL 'http://www.google.com/'
选项支持的数据类型
Gradle 限制了可用于声明命令行选项的数据类型。
命令行的使用因类型而异:
boolean
,Boolean
,Property<Boolean>
-
true
描述带有值或 的选项false
。
在命令行上传递选项会将值视为true
。例如,--foo
等于true
。
如果缺少该选项,则使用该属性的默认值。对于每个布尔选项,都会自动创建一个相反的选项。例如,--no-foo
是为提供的选项创建的--foo
,--bar
是为 创建的--no-bar
。名称以 开头的选项--no
是禁用选项,并将选项值设置为false
。仅当任务不存在同名选项时才会创建相反的选项。 Double
,Property<Double>
-
描述具有双精度值的选项。
在命令行上传递选项还需要一个值,例如--factor=2.2
或--factor 2.2
。 Integer
,Property<Integer>
-
描述具有整数值的选项。
在命令行上传递选项还需要一个值,例如--network-timeout=5000
或--network-timeout 5000
。 Long
,Property<Long>
-
描述具有长值的选项。
在命令行上传递选项还需要一个值,例如--threshold=2147483648
或--threshold 2147483648
。 String
,Property<String>
-
描述具有任意字符串值的选项。
在命令行上传递选项还需要一个值,例如--container-id=2x94held
或--container-id 2x94held
。 enum
,Property<enum>
-
将选项描述为枚举类型。
在命令行上传递选项还需要一个值,例如--log-level=DEBUG
或--log-level debug
。
该值不区分大小写。 List<T>
哪里,,,,T
Double
Integer
Long
String
enum
-
描述可以采用给定类型的多个值的选项。
选项的值必须作为多个声明提供,例如--image-id=123 --image-id=456
.
目前不支持其他表示法,例如逗号分隔的列表或由空格字符分隔的多个值。 ListProperty<T>
,SetProperty<T>
哪里,,,,T
Double
Integer
Long
String
enum
-
描述可以采用给定类型的多个值的选项。
选项的值必须作为多个声明提供,例如--image-id=123 --image-id=456
.
目前不支持其他表示法,例如逗号分隔的列表或由空格字符分隔的多个值。 DirectoryProperty
,RegularFileProperty
-
描述带有文件系统元素的选项。
在命令行上传递选项还需要一个表示路径的值,例如--output-file=file.txt
或--output-dir outputDir
。
相对路径是相对于拥有此属性实例的项目的项目目录进行解析的。看FileSystemLocationProperty.set()
。
记录选项的可用值
理论上,属性类型String
或选项List<String>
可以接受任何任意值。可以借助注释OptionValues以编程方式记录此类选项的可接受值:
@OptionValues('file')
可以将此注释分配给返回List
受支持数据类型之一的任何方法。您需要指定一个选项标识符来指示选项和可用值之间的关系。
笔记
|
在命令行上传递选项不支持的值不会使构建失败或引发异常。您必须在任务操作中实现此类行为的自定义逻辑。 |
下面的示例演示了对单个任务使用多个选项。任务实现提供了选项的可用值列表output-type
:
import org.gradle.api.tasks.options.Option;
import org.gradle.api.tasks.options.OptionValues;
public abstract class UrlProcess extends DefaultTask {
private String url;
private OutputType outputType;
@Input
@Option(option = "http", description = "Configures the http protocol to be allowed.")
public abstract Property<Boolean> getHttp();
@Option(option = "url", description = "Configures the URL to send the request to.")
public void setUrl(String url) {
if (!getHttp().getOrElse(true) && url.startsWith("http://")) {
throw new IllegalArgumentException("HTTP is not allowed");
} else {
this.url = url;
}
}
@Input
public String getUrl() {
return url;
}
@Option(option = "output-type", description = "Configures the output type.")
public void setOutputType(OutputType outputType) {
this.outputType = outputType;
}
@OptionValues("output-type")
public List<OutputType> getAvailableOutputTypes() {
return new ArrayList<OutputType>(Arrays.asList(OutputType.values()));
}
@Input
public OutputType getOutputType() {
return outputType;
}
@TaskAction
public void process() {
getLogger().quiet("Writing out the URL response from '{}' to '{}'", url, outputType);
// retrieve content from URL and write to output
}
private static enum OutputType {
CONSOLE, FILE
}
}
列出命令行选项
使用注释Option和OptionValues的命令行选项是自记录的。
$ gradle -q help --task processUrl Detailed task information for processUrl Path :processUrl Type UrlProcess (UrlProcess) Options --http Configures the http protocol to be allowed. --no-http Disables option --http. --output-type Configures the output type. Available values are: CONSOLE FILE --url Configures the URL to send the request to. --rerun Causes the task to be re-run even if up-to-date. Description - Group -
局限性
目前对声明命令行选项的支持存在一些限制。
-
只能通过注释为自定义任务声明命令行选项。没有用于定义选项的编程等效项。
-
选项不能全局声明,例如,在项目级别或作为插件的一部分。
-
当在命令行上分配选项时,需要明确说明暴露该选项的任务,例如,即使该任务依赖于该任务
gradle check --tests abc
,也不起作用。check
test
-
如果您指定的任务选项名称与内置 Gradle 选项的名称冲突,请
--
在调用任务之前使用分隔符来引用该选项。有关详细信息,请参阅消除任务选项与内置选项的歧义。
验证失败
通常,任务执行期间引发的异常会导致失败并立即终止构建。任务的结果将为FAILED
,构建的结果将为FAILED
,并且不会执行进一步的任务。当使用标志运行--continue
时,Gradle 在遇到任务失败后将继续运行构建中的其他请求的任务。但是,任何依赖于失败任务的任务都不会被执行。
有一种特殊类型的异常,当下游任务仅依赖于失败任务的输出时,其行为会有所不同。任务可以抛出VerificationException的子类型来指示它以受控方式失败,以便其输出对使用者仍然有效。当一个任务直接依赖于另一个任务的结果时,它依赖于另一个任务的结果dependsOn
。当 Gradle 使用 运行时--continue
,依赖于生产者任务输出(通过任务输入和输出之间的关系)的消费者任务在消费者失败后仍然可以运行。
例如,失败的单元测试将导致测试任务失败。但是,这并不能阻止另一个任务读取和处理该任务生成的(有效)测试结果。验证失败正是以这种方式使用的Test Report Aggregation Plugin
。
验证失败对于即使在产生其他任务可消耗的有用输出之后也需要报告失败的任务也很有用。
val process = tasks.register("process") {
val outputFile = layout.buildDirectory.file("processed.log")
outputs.files(outputFile) // (1)
doLast {
val logFile = outputFile.get().asFile
logFile.appendText("Step 1 Complete.") // (2)
throw VerificationException("Process failed!") // (3)
logFile.appendText("Step 2 Complete.") // (4)
}
}
tasks.register("postProcess") {
inputs.files(process) // (5)
doLast {
println("Results: ${inputs.files.singleFile.readText()}") // (6)
}
}
tasks.register("process") {
def outputFile = layout.buildDirectory.file("processed.log")
outputs.files(outputFile) // (1)
doLast {
def logFile = outputFile.get().asFile
logFile << "Step 1 Complete." // (2)
throw new VerificationException("Process failed!") // (3)
logFile << "Step 2 Complete." // (4)
}
}
tasks.register("postProcess") {
inputs.files(tasks.named("process")) // (5)
doLast {
println("Results: ${inputs.files.singleFile.text}") // (6)
}
}
$ gradle postProcess --continue > Task :process FAILED > Task :postProcess Results: Step 1 Complete. 2 actionable tasks: 2 executed FAILURE: Build failed with an exception.
-
注册输出:
process
任务将其输出写入日志文件。 -
修改输出:任务在执行时写入其输出文件。
-
任务失败:任务抛出 a
VerificationException
并在此时失败。 -
继续修改输出:由于异常停止任务,该行永远不会运行。
-
使用输出:由于使用任务的输出作为其自己的输入,因此
postProcess
任务取决于任务的输出。process
-
使用部分结果:设置标志后,尽管任务失败,
--continue
Gradle 仍会运行请求的任务。可以读取并显示部分(尽管仍然有效)结果。postProcess
process
postProcess
开发插件
了解插件
Gradle 附带了一套强大的核心系统,例如依赖管理、任务执行、项目配置等。但它能做的其他事情都是由插件提供的。
插件封装特定任务或集成的逻辑,例如编译代码、运行测试或部署工件。通过应用插件,用户可以轻松地将新功能添加到其构建过程中,而无需从头开始编写复杂的代码。
这种基于插件的方法使 Gradle 变得轻量级和模块化。它还促进了代码重用和可维护性,因为插件可以在项目之间或组织内共享。
插件介绍
插件可以来自 Gradle 或 Gradle 社区。但是,当用户想要组织他们的构建逻辑或需要现有插件未提供的特定构建功能时,他们可以开发自己的插件。
因此,我们区分三种不同类型的插件:
-
核心插件- 来自 Gradle 的插件。
-
社区插件- 来自Gradle 插件门户或公共存储库的插件。
-
本地或自定义插件- 您自己开发的插件。
社区插件
术语“社区插件”是指发布到 Gradle 插件门户(或其他公共存储库)的插件,例如Spotless 插件。
本地或自定义插件
术语本地或自定义插件是指您自己为自己的构建编写的插件。
自定义插件
自定义插件分为三种类型:
# | 类型 | 地点: | 最有可能的: | 益处: |
---|---|---|---|---|
构建脚本或脚本 |
本地插件 |
插件会自动编译并包含在构建脚本的类路径中。 |
||
一个约定插件 |
插件会自动编译、测试并在构建脚本的类路径上可用。该插件对于构建使用的每个构建脚本都是可见的。 |
|||
独立项目 |
一个共享插件 |
插件 JAR 已制作并发布。该插件可以在多个版本中使用并与其他人共享。 |
构建脚本和脚本插件
构建脚本插件通常是在构建文件中编写的小型本地插件,用于特定于单个构建或项目的任务,并且不需要在多个项目中重用。不推荐使用构建脚本插件,但许多其他形式的插件是从构建脚本插件演变而来的。
要创建 Gradle 插件,您需要在构建文件之一中编写一个实现Plugin接口的类。
以下示例创建一个GreetingPlugin
,它将hello
任务添加到项目中:
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.task("hello") {
doLast {
println("Hello from the GreetingPlugin")
}
}
}
}
// Apply the plugin
apply<GreetingPlugin>()
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
project.task('hello') {
doLast {
println 'Hello from the GreetingPlugin'
}
}
}
}
// Apply the plugin
apply plugin: GreetingPlugin
$ gradle -q hello Hello from the GreetingPlugin
该Project
对象作为参数传递apply()
,插件可以根据需要使用它来配置项目(例如添加任务、配置依赖项等)
脚本插件apply(from = " ")
与构建脚本插件类似,但是,插件定义只是在单独的脚本中完成,然后使用或将其应用于构建文件apply from: ''
:不建议使用脚本插件。
class GreetingScriptPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.task("hi") {
doLast {
println("Hi from the GreetingScriptPlugin")
}
}
}
}
// Apply the plugin
apply<GreetingScriptPlugin>()
class GreetingScriptPlugin implements Plugin<Project> {
void apply(Project project) {
project.task('hi') {
doLast {
println 'Hi from the GreetingScriptPlugin'
}
}
}
}
// Apply the plugin
apply plugin: GreetingScriptPlugin
apply(from = "other.gradle.kts")
apply from: 'other.gradle'
$ gradle -q hi Hi from the GreetingScriptPlugin
预编译脚本插件
预编译脚本插件在执行之前会被编译成类文件并打包成 JAR。这些插件使用 Groovy 或 Kotlin DSL,而不是纯 Java、Kotlin 或 Groovy。它们最好用作跨项目共享构建逻辑的约定插件,或者作为整齐组织构建逻辑的一种方式。
要创建预编译脚本插件,您可以:
-
使用 Gradle 的 Kotlin DSL - 插件是一个
.gradle.kts
文件,并应用id("kotlin-dsl")
. -
使用 Gradle 的 Groovy DSL - 该插件是一个
.gradle
文件,并应用id("groovy-gradle-plugin")
.
要应用预编译脚本插件,您需要知道其ID。 ID 源自插件脚本的文件名及其(可选)包声明。
例如,该脚本src/main/*/java-library.gradle(.kts)
的插件 ID 为java-library
(假设它没有包声明)。同样,只要它的包声明为 ,src/main/*/my/java-library.gradle(.kts)
就有一个插件 ID 。my.java-library
my
预编译脚本插件名称有两个重要的限制:
-
他们不能从 开始
org.gradle
。 -
它们不能与核心插件同名。
当插件应用于项目时,Gradle 会创建插件类的实例并调用该实例的Plugin.apply()方法。
笔记
|
Plugin 在应用该插件的每个项目中都会创建
一个新的 a 实例。 |
让我们将GreetingPlugin
脚本插件重写为预编译脚本插件。由于我们使用的是 Groovy 或 Kotlin DSL,因此该文件本质上成为插件。原始脚本插件只是创建了一个hello
打印问候语的任务,这就是我们将在预编译脚本插件中执行的操作:
tasks.register("hello") {
doLast {
println("Hello from the convention GreetingPlugin")
}
}
tasks.register("hello") {
doLast {
println("Hello from the convention GreetingPlugin")
}
}
现在可以使用其 ID将其GreetingPlugin
应用于其他子项目的构建中:
plugins {
application
id("GreetingPlugin")
}
plugins {
id 'application'
id('GreetingPlugin')
}
$ gradle -q hello Hello from the convention GreetingPlugin
约定插件
约定插件通常是预编译的脚本插件,它使用您自己的约定(即默认值)配置现有的核心和社区插件,例如使用java.toolchain.languageVersion = JavaLanguageVersion.of(17)
.约定插件还用于执行项目标准并帮助简化构建过程。他们可以应用和配置插件、创建新任务和扩展、设置依赖项等等。
让我们以包含三个子项目的构建为例:一个用于data-model
,一个用于database-logic
,一个用于app
代码。该项目具有以下结构:
.
├── buildSrc
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── data-model
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── database-logic
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── app
│ ├── src
│ │ └──...
│ └── build.gradle.kts
└── settings.gradle.kts
子项目的build文件database-logic
如下:
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm") version "1.9.23"
}
repositories {
mavenCentral()
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(11))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(11)
}
// More build logic
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm' version '1.9.23'
}
repositories {
mavenCentral()
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(11))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
// More build logic
我们应用该java-library
插件并添加该org.jetbrains.kotlin.jvm
插件以支持 Kotlin。我们还配置 Kotlin、Java、测试等。
我们的构建文件开始增长......
我们应用的插件越多,配置的插件越多,它就越大。app
和子项目的构建文件中也存在重复data-model
,尤其是在配置常见扩展(例如设置 Java 版本和 Kotlin 支持)时。
为了解决这个问题,我们使用约定插件。这使我们能够避免在每个构建文件中重复配置,并使我们的构建脚本更加简洁和可维护。在约定插件中,我们可以封装任意构建配置或自定义构建逻辑。
要开发约定插件,我们建议使用buildSrc
– 它代表完全独立的 Gradle 构建。
buildSrc
有自己的设置文件来定义此构建的依赖项所在的位置。
my-java-library.gradle.kts
我们在目录中添加一个名为的 Kotlin 脚本buildSrc/src/main/kotlin
。或者相反,在目录my-java-library.gradle
内调用 Groovy 脚本buildSrc/src/main/groovy
。我们将构建文件中的所有插件应用程序和配置放入database-logic
其中:
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
}
repositories {
mavenCentral()
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(11))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(11)
}
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
repositories {
mavenCentral()
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(11))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
该文件的名称my-java-library
是我们全新插件的 ID,我们现在可以在所有子项目中使用它。
提示
|
为什么版本id 'org.jetbrains.kotlin.jvm' 不见了?请参阅将外部插件应用于预编译脚本插件。
|
database-logic
通过删除所有冗余的构建逻辑并应用我们的约定插件,构建文件变得更加简单my-java-library
:
plugins {
id("my-java-library")
}
plugins {
id('my-java-library')
}
这个约定插件使我们能够轻松地在所有构建文件之间共享通用配置。任何修改都可以在一处进行,从而简化了维护。
二进制插件
Gradle 中的二进制插件是作为独立 JAR 文件构建并使用plugins{}
构建脚本中的块应用于项目的插件。
让我们将其转移GreetingPlugin
到一个独立的项目,以便我们可以发布它并与其他人共享。该插件本质上是从该buildSrc
文件夹移动到其自己的名为greeting-plugin
.
笔记
|
您可以从 发布插件buildSrc ,但不建议这样做。准备发布的插件应该是它们自己的版本。
|
greeting-plugin
只是一个生成包含插件类的 JAR 的 Java 项目。
将插件打包并发布到存储库的最简单方法是使用Gradle Plugin Development Plugin。该插件提供了必要的任务和配置(包括插件元数据),将您的脚本编译为可应用于其他版本的插件。
greeting-plugin
这是使用 Gradle Plugin 开发插件的项目的简单构建脚本:
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("simplePlugin") {
id = "org.example.greeting"
implementationClass = "org.example.GreetingPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
simplePlugin {
id = 'org.example.greeting'
implementationClass = 'org.example.GreetingPlugin'
}
}
}
有关发布插件的更多信息,请参阅发布插件。
项目 vs 设置 vs 初始化插件
这几种插件的区别在于它们的应用范围:
- 项目插件
-
项目插件是应用于构建中的特定项目的插件。它可以自定义构建逻辑、添加任务以及配置特定于项目的设置。
- 设置插件
-
settings.gradle
设置插件是在或文件中应用的插件settings.gradle.kts
。它可以配置适用于整个构建的设置,例如定义构建中包含哪些项目、配置构建脚本存储库以及将通用配置应用于所有项目。 - 初始化插件
-
init.gradle
init 插件是在或文件中应用的插件init.gradle.kts
。它可以配置全局适用于计算机上所有 Gradle 构建的设置,例如配置 Gradle 版本、设置默认存储库或将通用插件应用于所有构建。
了解插件的实施选项
脚本、预编译脚本或二进制插件之间的选择取决于您的具体要求和偏好。
脚本插件简单且易于编写,因为它们直接编写在构建脚本中。它们是用 Kotlin DSL 或 Groovy DSL 编写的。它们适合小型、一次性任务或快速实验。然而,随着构建脚本的大小和复杂性的增加,它们可能会变得难以维护。
预编译脚本插件是编译成打包在库中的 Java 类文件的 Kotlin DSL 脚本。与脚本插件相比,它们提供更好的性能和可维护性,并且可以在不同的项目中重用。您也可以用 Groovy DSL 编写它们,但不建议这样做。
二进制插件是用 Java 或 Kotlin 编写的成熟插件,编译成 JAR 文件并发布到存储库。它们提供最佳的性能、可维护性和可重用性。它们适用于需要在项目、构建和团队之间共享的复杂构建逻辑。您也可以用 Scala 或 Groovy 编写它们,但不建议这样做。
以下是实现 Gradle 插件的所有选项的细分:
# | 使用: | 类型: | 该插件是: | 通过应用创建: | 受到推崇的? |
---|---|---|---|---|---|
1 |
KotlinDSL |
脚本插件 |
作为实现接口方法的 |
|
否[ 1 ] |
2 |
Groovy DSL |
脚本插件 |
作为实现接口方法的 |
|
否[ 1 ] |
3 |
KotlinDSL |
预编译脚本插件 |
一份 |
|
是的[ 2 ] |
4 |
Groovy DSL |
预编译脚本插件 |
一份 |
|
否[ 3 ] |
5 |
KotlinDSL |
二进制插件 |
一份 |
|
是的[ 2 ] |
6 |
Groovy DSL |
二进制插件 |
一份 |
|
没有[ 2 ] |
7 |
Java |
二进制插件 |
|
|
是的[ 2 ] |
8 |
Kotlin |
二进制插件 |
|
|
是的[ 2 ] |
9 |
Groovy |
二进制插件 |
|
|
没有[ 2 ] |
10 |
Scala |
二进制插件 |
|
|
没有[ 2 ] |
实现预编译脚本插件
预编译脚本插件通常是 Kotlin 脚本,已编译并作为打包在库中的 Java 类文件分发。这些脚本旨在作为二进制 Gradle 插件使用,并建议用作约定插件。
约定插件是通常使用您自己的约定(即默认值)配置现有核心和社区插件的插件,例如使用java.toolchain.languageVersion = JavaLanguageVersion.of(17)
.约定插件还用于执行项目标准并帮助简化构建过程。他们可以应用和配置插件、创建新任务和扩展、设置依赖项等等。
设置插件ID
预编译脚本的插件 ID 源自其文件名和可选包声明。
例如,code-quality.gradle(.kts)
位于src/main/groovy
(或src/main/kotlin
) 中但没有包声明的名为的脚本将作为code-quality
插件公开:
plugins {
id("code-quality")
}
另一方面,code-quality.gradle(.kts)
位于src/main/groovy/my
(或src/main/kotlin/my
) 中且带有包声明的名为的脚本my
将作为my.code-quality
插件公开:
plugins {
id("my.code-quality")
}
处理文件
让我们首先创建一个名为的约定插件greetings
:
// Create extension object
interface GreetingPluginExtension {
val message: Property<String>
}
// Create extension object
interface GreetingPluginExtension {
Property<String> getMessage()
}
您可以在使用文件中找到有关延迟处理文件的更多信息。
使用扩展使插件可配置
扩展对象通常在插件中用于公开配置选项和附加功能来构建脚本。
当您应用定义扩展的插件时,您可以访问扩展对象并配置其属性或调用其方法来自定义插件的行为或插件提供的任务。
项目有一个关联的ExtensionContainer对象,其中包含已应用于项目的插件的所有设置和属性。您可以通过向此容器添加扩展对象来为您的插件提供配置。
让我们更新我们的greetings
示例:
// Create extension object
interface GreetingPluginExtension {
val message: Property<String>
}
// Add the 'greeting' extension object to project
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
// Create extension object
interface GreetingPluginExtension {
Property<String> getMessage()
}
// Add the 'greeting' extension object to project
def extension = project.extensions.create("greeting", GreetingPluginExtension)
message
您可以直接使用 来设置属性的值extension.message.set("Hi from Gradle,")
。
但是,该GreetingPluginExtension
对象可用作与扩展对象同名的项目属性。您现在可以message
像这样访问:
// Where the<GreetingPluginExtension>() is equivalent to project.extensions.getByType(GreetingPluginExtension::class.java)
the<GreetingPluginExtension>().message.set("Hi from Gradle")
extensions.findByType(GreetingPluginExtension).message.set("Hi from Gradle")
如果您应用该greetings
插件,您可以在构建脚本中设置约定:
plugins {
application
id("greetings")
}
greeting {
message = "Hello from Gradle"
}
plugins {
id 'application'
id('greetings')
}
configure(greeting) {
message = "Hello from Gradle"
}
添加默认配置作为约定
在插件中,您可以使用对象定义默认值,也称为约定project
。
约定属性是使用默认值初始化但可以覆盖的属性:
// Create extension object
interface GreetingPluginExtension {
val message: Property<String>
}
// Add the 'greeting' extension object to project
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
// Set a default value for 'message'
extension.message.convention("Hello from Gradle")
// Create extension object
interface GreetingPluginExtension {
Property<String> getMessage()
}
// Add the 'greeting' extension object to project
def extension = project.extensions.create("greeting", GreetingPluginExtension)
// Set a default value for 'message'
extension.message.convention("Hello from Gradle")
extension.message.convention(…)
message
为扩展的属性设置约定。此约定指定 的值应默认为位于项目构建目录中的message
名为的文件的内容。defaultGreeting.txt
如果message
未显式设置该属性,则其值将自动设置为 的内容defaultGreeting.txt
。
将扩展属性映射到任务属性
使用扩展并将其映射到自定义任务的输入/输出属性在插件中很常见。
在此示例中, 的 message 属性GreetingPluginExtension
映射到GreetingTask
作为输入的 的 message 属性:
// Create extension object
interface GreetingPluginExtension {
val message: Property<String>
}
// Add the 'greeting' extension object to project
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
// Set a default value for 'message'
extension.message.convention("Hello from Gradle")
// Create a greeting task
abstract class GreetingTask : DefaultTask() {
@Input
val message = project.objects.property<String>()
@TaskAction
fun greet() {
println("Message: ${message.get()}")
}
}
// Register the task and set the convention
tasks.register<GreetingTask>("hello") {
message.convention(extension.message)
}
// Create extension object
interface GreetingPluginExtension {
Property<String> getMessage()
}
// Add the 'greeting' extension object to project
def extension = project.extensions.create("greeting", GreetingPluginExtension)
// Set a default value for 'message'
extension.message.convention("Hello from Gradle")
// Create a greeting task
abstract class GreetingTask extends DefaultTask {
@Input
abstract Property<String> getMessage()
@TaskAction
void greet() {
println("Message: ${message.get()}")
}
}
// Register the task and set the convention
tasks.register("hello", GreetingTask) {
message.convention(extension.message)
}
$ gradle -q hello Message: Hello from Gradle
这意味着对扩展message
属性的更改将触发任务被视为过期,从而确保使用新消息重新执行任务。
您可以在延迟配置中找到有关可在任务实现和扩展中使用的类型的更多信息。
应用外部插件
为了在预编译脚本插件中应用外部插件,必须将其添加到插件构建文件中插件项目的实现类路径中:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.bmuschko:gradle-docker-plugin:6.4.0")
}
plugins {
id 'groovy-gradle-plugin'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.bmuschko:gradle-docker-plugin:6.4.0'
}
然后可以将其应用到预编译脚本插件中:
plugins {
id("com.bmuschko.docker-remote-api")
}
plugins {
id 'com.bmuschko.docker-remote-api'
}
这种情况下的插件版本是在依赖声明中定义的。
实施二进制插件
二进制插件是指以 JAR 文件形式编译和分发的插件。这些插件通常用 Java 或 Kotlin 编写,并为 Gradle 构建提供自定义功能或任务。
使用插件开发插件
Gradle Plugin Development 插件可用于协助开发 Gradle 插件。
该插件将自动应用Java Plugin,将gradleApi()
依赖项添加到api
配置中,在生成的 JAR 文件中生成所需的插件描述符,并配置发布时要使用的插件标记工件。
要应用和配置该插件,请将以下代码添加到您的构建文件中:
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("simplePlugin") {
id = "org.example.greeting"
implementationClass = "org.example.GreetingPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
simplePlugin {
id = 'org.example.greeting'
implementationClass = 'org.example.GreetingPlugin'
}
}
}
开发插件时建议编写和使用自定义任务类型,因为它会自动受益于增量构建。将插件应用到项目的另一个好处是,该任务validatePlugins
会自动检查自定义任务类型实现中定义的每个公共属性的现有输入/输出注释。
创建插件 ID
插件 ID 是全局唯一的,类似于 Java 包名称(即反向域名)。这种格式有助于防止命名冲突,并允许将具有相似所有权的插件分组。
显式插件标识符简化了将插件应用到项目的过程。您的插件 ID 应结合反映命名空间(指向您或您的组织的合理指针)及其提供的插件名称的组件。例如,如果您的 Github 帐户名为foo
且您的插件名为bar
,则合适的插件 ID 可能是com.github.foo.bar
。同样,如果插件是在baz
组织中开发的,则插件 ID 可能是org.baz.bar
。
插件 ID 应遵循以下准则:
-
可以包含任何字母数字字符、“.”和“-”。
-
必须至少包含一个“.”将命名空间与插件名称分隔开的字符。
-
通常,命名空间使用小写反向域名约定。
-
通常,名称中仅使用小写字符。
-
org.gradle
不能使用、com.gradle
、 和命名空间。com.gradleware
-
不能以“.”开头或结尾特点。
-
不能包含连续的“.”字符(即“..”)。
标识所有权和名称的命名空间对于插件 ID 来说就足够了。
将多个插件捆绑在单个 JAR 工件中时,建议遵循相同的命名约定。这种做法有助于对相关插件进行逻辑分组。
单个项目中可以定义和注册(通过不同标识符)的插件数量没有限制。
作为类编写的插件的标识符应在包含插件类的项目构建脚本中定义。为此,java-gradle-plugin
需要应用:
plugins {
id("java-gradle-plugin")
}
gradlePlugin {
plugins {
create("androidApplicationPlugin") {
id = "com.android.application"
implementationClass = "com.android.AndroidApplicationPlugin"
}
create("androidLibraryPlugin") {
id = "com.android.library"
implementationClass = "com.android.AndroidLibraryPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
androidApplicationPlugin {
id = 'com.android.application'
implementationClass = 'com.android.AndroidApplicationPlugin'
}
androidLibraryPlugin {
id = 'com.android.library'
implementationClass = 'com.android.AndroidLibraryPlugin'
}
}
}
处理文件
开发插件时,在接受文件位置的输入配置时保持灵活性是个好主意。
建议使用 Gradle 的托管属性并project.layout
选择文件或目录位置。这将启用延迟配置,以便仅在需要文件时才会解析实际位置,并且可以在构建配置期间随时重新配置。
此 Gradle 构建文件定义了一个GreetingToFileTask
将问候语写入文件的任务。它还注册了两个任务:greet
,它创建带有问候语的文件,以及sayGreeting
,它打印文件的内容。该greetingFile
属性用于指定问候语的文件路径:
abstract class GreetingToFileTask : DefaultTask() {
@get:OutputFile
abstract val destination: RegularFileProperty
@TaskAction
fun greet() {
val file = destination.get().asFile
file.parentFile.mkdirs()
file.writeText("Hello!")
}
}
val greetingFile = objects.fileProperty()
tasks.register<GreetingToFileTask>("greet") {
destination = greetingFile
}
tasks.register("sayGreeting") {
dependsOn("greet")
val greetingFile = greetingFile
doLast {
val file = greetingFile.get().asFile
println("${file.readText()} (file: ${file.name})")
}
}
greetingFile = layout.buildDirectory.file("hello.txt")
abstract class GreetingToFileTask extends DefaultTask {
@OutputFile
abstract RegularFileProperty getDestination()
@TaskAction
def greet() {
def file = getDestination().get().asFile
file.parentFile.mkdirs()
file.write 'Hello!'
}
}
def greetingFile = objects.fileProperty()
tasks.register('greet', GreetingToFileTask) {
destination = greetingFile
}
tasks.register('sayGreeting') {
dependsOn greet
doLast {
def file = greetingFile.get().asFile
println "${file.text} (file: ${file.name})"
}
}
greetingFile = layout.buildDirectory.file('hello.txt')
$ gradle -q sayGreeting Hello! (file: hello.txt)
在此示例中,我们将greet
任务destination
属性配置为闭包/提供程序,并使用Project.file(java.lang.Object)File
方法对其进行评估,以在最后一刻将闭包/提供程序的返回值转换为对象。请注意,我们在任务配置之后greetingFile
指定属性值。这种惰性评估是在设置文件属性时接受任何值然后在读取属性时解析该值的一个关键好处。
您可以在使用文件中了解有关延迟处理文件的更多信息。
使用扩展使插件可配置
大多数插件都提供构建脚本和其他插件的配置选项,以自定义插件的工作方式。插件使用扩展对象来完成此操作。
项目有一个关联的ExtensionContainer对象,其中包含已应用于项目的插件的所有设置和属性。您可以通过向此容器添加扩展对象来为您的插件提供配置。
扩展对象只是一个具有表示配置的 Java Bean 属性的对象。
让我们greeting
向项目添加一个扩展对象,它允许您配置问候语:
interface GreetingPluginExtension {
val message: Property<String>
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Add the 'greeting' extension object
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
// Add a task that uses configuration from the extension object
project.task("hello") {
doLast {
println(extension.message.get())
}
}
}
}
apply<GreetingPlugin>()
// Configure the extension
the<GreetingPluginExtension>().message = "Hi from Gradle"
interface GreetingPluginExtension {
Property<String> getMessage()
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Add the 'greeting' extension object
def extension = project.extensions.create('greeting', GreetingPluginExtension)
// Add a task that uses configuration from the extension object
project.task('hello') {
doLast {
println extension.message.get()
}
}
}
}
apply plugin: GreetingPlugin
// Configure the extension
greeting.message = 'Hi from Gradle'
$ gradle -q hello Hi from Gradle
在此示例中,GreetingPluginExtension
是一个具有名为 的属性的对象message
。扩展对象将添加到名为 的项目中greeting
。该对象可用作与扩展对象同名的项目属性。
the<GreetingPluginExtension>()
相当于project.extensions.getByType(GreetingPluginExtension::class.java)
.
通常,您需要在单个插件上指定多个相关属性。 Gradle 为每个扩展对象添加了一个配置块,因此您可以对设置进行分组:
interface GreetingPluginExtension {
val message: Property<String>
val greeter: Property<String>
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
project.task("hello") {
doLast {
println("${extension.message.get()} from ${extension.greeter.get()}")
}
}
}
}
apply<GreetingPlugin>()
// Configure the extension using a DSL block
configure<GreetingPluginExtension> {
message = "Hi"
greeter = "Gradle"
}
interface GreetingPluginExtension {
Property<String> getMessage()
Property<String> getGreeter()
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create('greeting', GreetingPluginExtension)
project.task('hello') {
doLast {
println "${extension.message.get()} from ${extension.greeter.get()}"
}
}
}
}
apply plugin: GreetingPlugin
// Configure the extension using a DSL block
greeting {
message = 'Hi'
greeter = 'Gradle'
}
$ gradle -q hello Hi from Gradle
在此示例中,可以将多个设置分组在configure<GreetingPluginExtension>
块内。该configure
函数用于配置扩展对象。它提供了一种设置属性或将配置应用于这些对象的便捷方法。构建脚本的configure
函数 ( )中使用的类型GreetingPluginExtension
必须与扩展类型匹配。然后,当块被执行时,块的接收者就是扩展。
在此示例中,可以将多个设置分组在greeting
闭包内。构建脚本 ( ) 中闭包块的名称greeting
必须与扩展对象名称匹配。然后,当执行闭包时,扩展对象上的字段将根据标准 Groovy 闭包委托功能映射到闭包内的变量。
声明 DSL 配置容器
使用扩展对象扩展Gradle DSL 来为插件添加项目属性和 DSL 块。由于扩展对象是常规对象,因此您可以通过向扩展对象添加属性和方法来提供嵌套在插件块内的自己的 DSL。
为了说明目的,我们考虑以下构建脚本。
plugins {
id("org.myorg.server-env")
}
environments {
create("dev") {
url = "http://localhost:8080"
}
create("staging") {
url = "http://staging.enterprise.com"
}
create("production") {
url = "http://prod.enterprise.com"
}
}
plugins {
id 'org.myorg.server-env'
}
environments {
dev {
url = 'http://localhost:8080'
}
staging {
url = 'http://staging.enterprise.com'
}
production {
url = 'http://prod.enterprise.com'
}
}
插件公开的 DSL 公开了一个用于定义一组环境的容器。用户配置的每个环境都有一个任意但声明性的名称,并用其自己的 DSL 配置块表示。上面的示例实例化了开发、暂存和生产环境,包括其各自的 URL。
每个环境都必须有代码中的数据表示形式来捕获值。环境的名称是不可变的,可以作为构造函数参数传入。目前,数据对象存储的唯一其他参数是 URL。
以下ServerEnvironment
对象满足这些要求:
abstract public class ServerEnvironment {
private final String name;
@javax.inject.Inject
public ServerEnvironment(String name) {
this.name = name;
}
public String getName() {
return name;
}
abstract public Property<String> getUrl();
}
Gradle 公开工厂方法 ObjectFactory.domainObjectContainer(Class, NamedDomainObjectFactory) 来创建数据对象的容器。该方法采用的参数是表示数据的类。创建的NamedDomainObjectContainer类型实例可以通过将其添加到具有特定名称的扩展容器来向最终用户公开。
插件通常会在插件实现中对捕获的值进行后处理,例如配置任务:
public class ServerEnvironmentPlugin implements Plugin<Project> {
@Override
public void apply(final Project project) {
ObjectFactory objects = project.getObjects();
NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer =
objects.domainObjectContainer(ServerEnvironment.class, name -> objects.newInstance(ServerEnvironment.class, name));
project.getExtensions().add("environments", serverEnvironmentContainer);
serverEnvironmentContainer.all(serverEnvironment -> {
String env = serverEnvironment.getName();
String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
String taskName = "deployTo" + capitalizedServerEnv;
project.getTasks().register(taskName, Deploy.class, task -> task.getUrl().set(serverEnvironment.getUrl()));
});
}
}
在上面的示例中,为每个用户配置的环境动态创建部署任务。
您可以在开发自定义 Gradle 类型中找到有关实现项目扩展的更多信息。
对类似 DSL 的 API 进行建模
插件公开的 DSL 应该可读且易于理解。
例如,让我们考虑插件提供的以下扩展。在当前形式中,它提供了一个“平面”属性列表,用于配置网站的创建:
plugins {
id("org.myorg.site")
}
site {
outputDir = layout.buildDirectory.file("mysite")
websiteUrl = "https://gradle.org"
vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
plugins {
id 'org.myorg.site'
}
site {
outputDir = layout.buildDirectory.file("mysite")
websiteUrl = 'https://gradle.org'
vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}
随着公开属性数量的增加,您应该引入一个嵌套的、更具表现力的结构。
以下代码片段添加了一个名为customData
扩展的一部分的新配置块。这更有力地表明了这些属性的含义:
plugins {
id("org.myorg.site")
}
site {
outputDir = layout.buildDirectory.file("mysite")
customData {
websiteUrl = "https://gradle.org"
vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
}
plugins {
id 'org.myorg.site'
}
site {
outputDir = layout.buildDirectory.file("mysite")
customData {
websiteUrl = 'https://gradle.org'
vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}
}
实现此类扩展的支持对象很简单。首先,引入一个新的数据对象来管理属性websiteUrl
和vcsUrl
:
abstract public class CustomData {
abstract public Property<String> getWebsiteUrl();
abstract public Property<String> getVcsUrl();
}
在扩展中,创建CustomData
类的实例和方法以将捕获的值委托给数据实例。
要配置基础数据对象,请定义Action类型的参数。
Action
以下示例演示了在扩展定义中的使用:
abstract public class SiteExtension {
abstract public RegularFileProperty getOutputDir();
@Nested
abstract public CustomData getCustomData();
public void customData(Action<? super CustomData> action) {
action.execute(getCustomData());
}
}
将扩展属性映射到任务属性
插件通常使用扩展来捕获来自构建脚本的用户输入并将其映射到自定义任务的输入/输出属性。构建脚本作者与扩展的 DSL 交互,而插件实现则处理底层逻辑:
// Extension class to capture user input
class MyExtension {
@Input
var inputParameter: String? = null
}
// Custom task that uses the input from the extension
class MyCustomTask : org.gradle.api.DefaultTask() {
@Input
var inputParameter: String? = null
@TaskAction
fun executeTask() {
println("Input parameter: $inputParameter")
}
}
// Plugin class that configures the extension and task
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create and configure the extension
val extension = project.extensions.create("myExtension", MyExtension::class.java)
// Create and configure the custom task
project.tasks.register("myTask", MyCustomTask::class.java) {
group = "custom"
inputParameter = extension.inputParameter
}
}
}
// Extension class to capture user input
class MyExtension {
@Input
String inputParameter = null
}
// Custom task that uses the input from the extension
class MyCustomTask extends DefaultTask {
@Input
String inputParameter = null
@TaskAction
def executeTask() {
println("Input parameter: $inputParameter")
}
}
// Plugin class that configures the extension and task
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
// Create and configure the extension
def extension = project.extensions.create("myExtension", MyExtension)
// Create and configure the custom task
project.tasks.register("myTask", MyCustomTask) {
group = "custom"
inputParameter = extension.inputParameter
}
}
}
在此示例中,MyExtension
该类定义了一个inputParameter
可以在构建脚本中设置的属性。该类MyPlugin
配置此扩展并使用其inputParameter
值来配置MyCustomTask
任务。然后,任务MyCustomTask
在其逻辑中使用此输入参数。
您可以在延迟配置中了解有关可在任务实现和扩展中使用的类型的更多信息。
添加带有约定的默认配置
插件应该在特定上下文中提供合理的默认值和标准,从而减少用户需要做出的决策数量。使用该project
对象,您可以定义默认值。这些被称为约定。
约定是使用默认值初始化的属性,并且可以由用户在其构建脚本中覆盖。例如:
interface GreetingPluginExtension {
val message: Property<String>
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Add the 'greeting' extension object
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
extension.message.convention("Hello from GreetingPlugin")
// Add a task that uses configuration from the extension object
project.task("hello") {
doLast {
println(extension.message.get())
}
}
}
}
apply<GreetingPlugin>()
interface GreetingPluginExtension {
Property<String> getMessage()
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Add the 'greeting' extension object
def extension = project.extensions.create('greeting', GreetingPluginExtension)
extension.message.convention('Hello from GreetingPlugin')
// Add a task that uses configuration from the extension object
project.task('hello') {
doLast {
println extension.message.get()
}
}
}
}
apply plugin: GreetingPlugin
$ gradle -q hello Hello from GreetingPlugin
在此示例中,GreetingPluginExtension
是一个表示约定的类。 message 属性是约定属性,默认值为“Hello from GreetingPlugin”。
用户可以在其构建脚本中覆盖此值:
GreetingPluginExtension {
message = "Custom message"
}
GreetingPluginExtension {
message = 'Custom message'
}
$ gradle -q hello
Custom message
将功能与约定分开
将插件中的功能与约定分开允许用户选择要应用的任务和约定。
例如,Java Base 插件提供了非固定(即通用)功能,如SourceSets
,而 Java 插件添加了 Java 开发人员熟悉的任务和约定,如classes
、jar
或javadoc
。
在设计自己的插件时,请考虑开发两个插件 - 一个用于功能,另一个用于约定 - 为用户提供灵活性。
在下面的示例中,MyPlugin
包含约定并MyBasePlugin
定义功能。然后,MyPlugin
应用MyBasePlugin
,这就是所谓的插件组合。要应用另一个插件的插件:
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyBasePlugin implements Plugin<Project> {
public void apply(Project project) {
// define capabilities
}
}
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getPlugins().apply(MyBasePlugin.class);
// define conventions
}
}
对插件做出反应
Gradle 插件实现中的一个常见模式是配置构建中现有插件和任务的运行时行为。
例如,插件可以假设它应用于基于 Java 的项目并自动重新配置标准源目录:
public class InhouseStrongOpinionConventionJavaPlugin implements Plugin<Project> {
public void apply(Project project) {
// Careful! Eagerly appyling plugins has downsides, and is not always recommended.
project.getPlugins().apply(JavaPlugin.class);
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
main.getJava().setSrcDirs(Arrays.asList("src"));
}
}
这种方法的缺点是它会自动强制项目应用Java插件,对其施加强烈的意见(即降低灵活性和通用性)。实际上,应用该插件的项目甚至可能不处理 Java 代码。
该插件可以对使用项目应用 Java 插件的事实做出反应,而不是自动应用 Java 插件。只有在这种情况下,才会应用特定的配置:
public class InhouseConventionJavaPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
main.getJava().setSrcDirs(Arrays.asList("src"));
});
}
}
如果没有充分的理由假设使用的项目具有预期的设置,则对插件做出反应优于应用插件。
同样的概念也适用于任务类型:
public class InhouseConventionWarPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getTasks().withType(War.class).configureEach(war ->
war.setWebXml(project.file("src/someWeb.xml")));
}
}
响应构建功能
有两个主要用例:
-
在报告或统计数据中使用构建功能的状态。
-
通过禁用不兼容的插件功能来逐步采用实验性 Gradle 功能。
下面是一个使用这两种情况的插件示例。
public abstract class MyPlugin implements Plugin<Project> {
@Inject
protected abstract BuildFeatures getBuildFeatures(); // (1)
@Override
public void apply(Project p) {
BuildFeatures buildFeatures = getBuildFeatures();
Boolean configCacheRequested = buildFeatures.getConfigurationCache().getRequested() // (2)
.getOrNull(); // could be null if user did not opt in nor opt out
String configCacheUsage = describeFeatureUsage(configCacheRequested);
MyReport myReport = new MyReport();
myReport.setConfigurationCacheUsage(configCacheUsage);
boolean isolatedProjectsActive = buildFeatures.getIsolatedProjects().getActive() // (3)
.get(); // the active state is always defined
if (!isolatedProjectsActive) {
myOptionalPluginLogicIncompatibleWithIsolatedProjects();
}
}
private String describeFeatureUsage(Boolean requested) {
return requested == null ? "no preference" : requested ? "opt-in" : "opt-out";
}
private void myOptionalPluginLogicIncompatibleWithIsolatedProjects() {
}
}
-
该
BuildFeatures
服务可以注入插件、任务和其他托管类型。 -
访问
requested
功能的状态以进行报告。 -
使用
active
功能的状态来禁用不兼容的功能。
构建特征属性
状态BuildFeature
属性用类型表示Provider<Boolean>
。
构建功能的状态BuildFeature.getRequested()
决定用户是否请求启用或禁用该功能。
当requested
提供者值为:
-
true
— 用户选择使用该功能 -
false
— 用户选择不使用该功能 -
undefined
— 用户既没有选择加入也没有选择退出使用该功能
BuildFeature.getActive()
构建功能的状态始终是已定义的。它代表构建中功能的有效状态。
当active
提供者值为:
-
true
- 该功能可能会以特定于该功能的方式影响构建行为 -
false
— 该功能不会影响构建行为
请注意,active
状态不依赖于requested
状态。即使用户请求某个功能,由于构建中使用了其他构建选项,该功能仍然可能未激活。即使用户没有指定首选项,Gradle 也可以默认激活某项功能。
提供默认依赖项
插件的实现有时需要使用外部依赖项。
您可能希望使用 Gradle 的依赖管理机制自动下载工件,然后在插件中声明的任务类型的操作中使用它。理想情况下,插件实现不需要询问用户该依赖项的坐标 - 它可以简单地预定义一个合理的默认版本。
让我们看一个插件示例,该插件下载包含数据以供进一步处理的文件。插件实现声明了一个自定义配置,允许使用依赖坐标分配这些外部依赖项:
public class DataProcessingPlugin implements Plugin<Project> {
public void apply(Project project) {
Configuration dataFiles = project.getConfigurations().create("dataFiles", c -> {
c.setVisible(false);
c.setCanBeConsumed(false);
c.setCanBeResolved(true);
c.setDescription("The data artifacts to be processed for this plugin.");
c.defaultDependencies(d -> d.add(project.getDependencies().create("org.myorg:data:1.4.6")));
});
project.getTasks().withType(DataProcessing.class).configureEach(
dataProcessing -> dataProcessing.getDataFiles().from(dataFiles));
}
}
abstract public class DataProcessing extends DefaultTask {
@InputFiles
abstract public ConfigurableFileCollection getDataFiles();
@TaskAction
public void process() {
System.out.println(getDataFiles().getFiles());
}
}
这种方法对于最终用户来说很方便,因为不需要主动声明依赖项。该插件已经提供了有关此实现的所有详细信息。
但是如果用户想要重新定义默认依赖项怎么办?
没问题。该插件还公开了可用于分配不同依赖项的自定义配置。实际上,默认依赖项被覆盖:
plugins {
id("org.myorg.data-processing")
}
dependencies {
dataFiles("org.myorg:more-data:2.6")
}
plugins {
id 'org.myorg.data-processing'
}
dependencies {
dataFiles 'org.myorg:more-data:2.6'
}
您会发现此模式非常适合执行任务操作时需要外部依赖项的任务。您可以通过公开扩展属性(例如toolVersion
在JaCoCo 插件中)进一步抽象用于外部依赖项的版本
。
尽量减少外部库的使用
在 Gradle 项目中使用外部库可以带来很大的便利,但请注意它们可能会引入复杂的依赖关系图。 Gradle 的buildEnvironment
任务可以帮助您可视化这些依赖关系,包括插件的依赖关系。请记住,插件共享相同的类加载器,因此同一库的不同版本可能会出现冲突。
为了演示,我们假设以下构建脚本:
plugins {
id("org.asciidoctor.jvm.convert") version "4.0.2"
}
plugins {
id 'org.asciidoctor.jvm.convert' version '4.0.2'
}
任务的输出清楚地指示了classpath
配置的类路径:
$ gradle buildEnvironment > Task :buildEnvironment ------------------------------------------------------------ Root project 'external-libraries' ------------------------------------------------------------ classpath \--- org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:4.0.2 \--- org.asciidoctor:asciidoctor-gradle-jvm:4.0.2 +--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 | \--- org.tukaani:xz:1.6 +--- org.ysb33r.gradle:grolifant-herd:3.0.0 | +--- org.tukaani:xz:1.6 | +--- org.ysb33r.gradle:grolifant40:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.apache.commons:commons-collections4:4.4 | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 | | | +--- org.tukaani:xz:1.6 | | | +--- org.apache.commons:commons-collections4:4.4 | | | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant50:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant40-legacy-api:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.apache.commons:commons-collections4:4.4 | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant60:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant50:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant70:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant50:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant60:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant80:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant50:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant60:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant70:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) +--- org.asciidoctor:asciidoctor-gradle-base:4.0.2 | \--- org.ysb33r.gradle:grolifant-herd:3.0.0 (*) \--- org.asciidoctor:asciidoctorj-api:2.5.7 (*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation. A web-based, searchable dependency report is available by adding the --scan option. BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
Gradle 插件不会在其自己的独立类加载器中运行,因此您必须考虑是否确实需要一个库,或者一个更简单的解决方案是否足够。
对于作为任务执行的一部分执行的逻辑,请使用允许您隔离库的Worker API 。
提供插件的多个变体
配置其他插件变体的最便捷方法是使用功能变体,这是应用 Java 插件之一的所有 Gradle 项目中都可用的概念:
dependencies {
implementation 'com.google.guava:guava:30.1-jre' // Regular dependency
featureVariant 'com.google.guava:guava-gwt:30.1-jre' // Feature variant dependency
}
在以下示例中,每个插件变体都是单独开发的。为每个变体编译单独的源集并将其打包在单独的 jar 中。
以下示例演示了如何添加与 Gradle 7.0+ 兼容的变体,同时“主”变体与旧版本兼容:
val gradle7 = sourceSets.create("gradle7")
java {
registerFeature(gradle7.name) {
usingSourceSet(gradle7)
capability(project.group.toString(), project.name, project.version.toString()) // (1)
}
}
configurations.configureEach {
if (isCanBeConsumed && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, // (2)
objects.named("7.0"))
}
}
}
tasks.named<Copy>(gradle7.processResourcesTaskName) { // (3)
val copyPluginDescriptors = rootSpec.addChild()
copyPluginDescriptors.into("META-INF/gradle-plugins")
copyPluginDescriptors.from(tasks.pluginDescriptors)
}
dependencies {
"gradle7CompileOnly"(gradleApi()) // (4)
}
def gradle7 = sourceSets.create('gradle7')
java {
registerFeature(gradle7.name) {
usingSourceSet(gradle7)
capability(project.group.toString(), project.name, project.version.toString()) // (1)
}
}
configurations.configureEach {
if (canBeConsumed && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, // (2)
objects.named(GradlePluginApiVersion, '7.0'))
}
}
}
tasks.named(gradle7.processResourcesTaskName) { // (3)
def copyPluginDescriptors = rootSpec.addChild()
copyPluginDescriptors.into('META-INF/gradle-plugins')
copyPluginDescriptors.from(tasks.pluginDescriptors)
}
dependencies {
gradle7CompileOnly(gradleApi()) // (4)
}
笔记
|
只有 Gradle 版本 7 或更高版本可以明确作为变体的目标,因为仅在 Gradle 7 中添加了对此的支持。 |
首先,我们为 Gradle7 插件变体声明一个单独的源集和一个功能变体。然后,我们进行一些特定的连接,将该功能转换为适当的 Gradle 插件变体:
-
将与组件 GAV 相对应的隐式功能分配给变体。
-
将Gradle API 版本属性分配给Gradle7 变体的所有可用配置。 Gradle 使用此信息来确定在插件解析期间选择哪个变体。
-
配置
processGradle7Resources
任务以确保插件描述符文件添加到 Gradle7 变体 Jar 中。 -
为我们的新变体添加依赖项
gradleApi()
,以便 API 在编译期间可见。
请注意,目前没有方便的方法来访问其他 Gradle 版本的 API,就像您用来构建插件的 API 一样。理想情况下,每个变体都应该能够声明对其支持的最小 Gradle 版本的 API 的依赖关系。今后这一点将会得到改善。
上面的代码片段假设插件的所有变体都在同一位置具有插件类。也就是说,如果您的插件类是org.example.GreetingPlugin
,您需要在 中创建该类的第二个变体src/gradle7/java/org/example
。
使用多变体插件的版本特定变体
鉴于对多变体插件的依赖,Gradle 在解决以下任一问题时将自动选择与当前 Gradle 版本最匹配的变体:
-
plugins {}
块中指定的插件; -
buildscript
类路径依赖; -
出现在编译或运行时类路径上的构建源 (
buildSrc
)根项目中的依赖项; -
应用Java Gradle Plugin Development 插件或Kotlin DSL 插件的项目中的依赖项出现在编译或运行时类路径中。
最佳匹配变体是针对最高 Gradle API 版本且不超过当前构建的 Gradle 版本的变体。
在所有其他情况下,如果存在未指定支持的 Gradle API 版本的插件变体,则首选该变体。
在使用插件作为依赖项的项目中,可以请求支持不同 Gradle 版本的插件依赖项的变体。这允许依赖其他插件的多变体插件访问其 API,这些 API 专门在其特定于版本的变体中提供。
此代码片段使上面定义的插件变体gradle7
消耗其对其他多变体插件的依赖项的匹配变体:
configurations.configureEach {
if (isCanBeResolved && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
objects.named("7.0"))
}
}
}
configurations.configureEach {
if (canBeResolved && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
objects.named(GradlePluginApiVersion, '7.0'))
}
}
}
报告问题
插件可以通过 Gradle 的问题报告 API 报告问题。 API 报告有关构建过程中发生的问题的丰富的结构化信息。不同的用户界面(例如 Gradle 的控制台输出、构建扫描或 IDE)可以使用此信息以最合适的方式向用户传达问题。
以下示例显示了插件报告的问题:
public class ProblemReportingPlugin implements Plugin<Project> {
private final ProblemReporter problemReporter;
@Inject
public ProblemReportingPlugin(Problems problems) { // (1)
this.problemReporter = problems.forNamespace("org.myorg"); // (2)
}
public void apply(Project project) {
this.problemReporter.reporting(builder -> builder // (3)
.label("Plugin 'x' is deprecated")
.details("The plugin 'x' is deprecated since version 2.5")
.solution("Please use plugin 'y'")
.severity(Severity.WARNING)
);
}
}
-
该
Problem
服务被注入到插件中。 -
为插件创建了一个问题报告器。虽然命名空间由插件作者决定,但建议使用插件 ID。
-
报告有问题。此问题是可以恢复的,因此构建将继续。
有关完整示例,请参阅我们的端到端示例。
问题聚合
报告问题时,Gradle 会根据问题的类别标签,通过 Tooling API 发送类似的问题来进行聚合。
-
当报告问题时,第一次出现的问题将被报告为ProblemDescriptor,其中包含有关问题的完整信息。
-
任何后续出现的相同问题都将报告为ProblemAggregationDescriptor。该描述符将在构建结束时到达并包含问题发生的次数。
-
如果对于任何存储桶(即类别和标签配对),收集的出现次数大于 10.000,则将立即发送,而不是在构建结束时发送。
测试 Gradle 插件
测试通过确保可靠和高质量的软件在开发过程中发挥着至关重要的作用。此原则适用于构建代码,包括 Gradle 插件。
示例项目
本节围绕一个名为“URL 验证器插件”的示例项目展开。该插件创建一个名为verifyUrl
检查给定 URL 是否可以通过 HTTP GET 解析的任务。最终用户可以通过名为 的扩展名提供 URL verification
。
以下构建脚本假定插件 JAR 文件已发布到二进制存储库。该脚本演示了如何将插件应用到项目并配置其公开的扩展:
plugins {
id("org.myorg.url-verifier") // (1)
}
verification {
url = "https://www.google.com/" // (2)
}
plugins {
id 'org.myorg.url-verifier' // (1)
}
verification {
url = 'https://www.google.com/' // (2)
}
-
将插件应用到项目中
-
配置要通过公开的扩展进行验证的 URL
verifyUrl
如果对配置的 URL 的 HTTP GET 调用返回 200 响应代码,则执行任务会呈现成功消息:
$ gradle verifyUrl
> Task :verifyUrl
Successfully resolved URL 'https://www.google.com/'
BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed
在深入研究代码之前,我们首先回顾一下不同类型的测试以及支持实现它们的工具。
测试的重要性
测试是软件开发生命周期的重要组成部分,确保软件在发布前正确运行并满足质量标准。自动化测试使开发人员能够充满信心地重构和改进代码。
测试金字塔
- 手动测试
-
虽然手动测试很简单,但很容易出错并且需要人力。对于 Gradle 插件,手动测试涉及在构建脚本中使用插件。
- 自动化测试
-
自动化测试包括单元测试、集成测试和功能测试。
Mike Cohen 在他的《成功敏捷:使用 Scrum 进行软件开发》一书中介绍的测试金字塔描述了三种类型的自动化测试:
-
单元测试:单独验证最小的代码单元,通常是方法。它使用存根或模拟将代码与外部依赖项隔离。
-
集成测试:验证多个单元或组件是否可以协同工作。
-
功能测试:从最终用户的角度测试系统,确保功能正确。 Gradle 插件的端到端测试模拟构建、应用插件并执行特定任务以验证功能。
工装支持
使用适当的工具可以简化手动和自动测试 Gradle 插件的过程。下表提供了每种测试方法的摘要。您可以选择任何您喜欢的测试框架。
详细解释和代码示例请参考以下具体章节:
测试类型 | 工装支持 |
---|---|
任何基于 JVM 的测试框架 |
|
任何基于 JVM 的测试框架 |
|
任何基于 JVM 的测试框架和Gradle TestKit |
设置手动测试
Gradle 的复合构建功能使手动测试插件变得很容易。独立插件项目和使用项目可以组合成一个单元,从而可以轻松尝试或调试更改,而无需重新发布二进制文件:
. ├── include-plugin-build // (1) │ ├── build.gradle │ └── settings.gradle └── url-verifier-plugin // (2) ├── build.gradle ├── settings.gradle └── src
-
使用包含插件项目的项目
-
插件项目
有两种方法可以将插件项目包含在使用项目中:
-
通过使用命令行选项
--include-build
。 -
includeBuild
通过使用中的方法settings.gradle
。
以下代码片段演示了设置文件的使用:
pluginManagement {
includeBuild("../url-verifier-plugin")
}
pluginManagement {
includeBuild '../url-verifier-plugin'
}
verifyUrl
项目中任务的命令行输出include-plugin-build
看起来与简介中所示的完全相同,只是它现在作为复合构建的一部分执行。
手动测试在开发过程中占有一席之地,但它并不能替代自动化测试。
设置自动化测试
尽早设置一套测试对于插件的成功至关重要。当将插件升级到新的 Gradle 版本或增强/重构代码时,自动化测试成为宝贵的安全网。
整理测试源代码
我们建议实施良好的单元、集成和功能测试分布,以涵盖最重要的用例。自动分离每种测试类型的源代码会导致项目更易于维护和管理。
默认情况下,Java 项目创建一个用于在目录中组织单元测试的约定src/test/java
。另外,如果应用Groovy插件,则src/test/groovy
考虑编译该目录下的源代码(与该目录下的Kotlin标准相同src/test/kotlin
)。因此,其他测试类型的源代码目录应遵循类似的模式:
. └── src ├── functionalTest │ └── groovy // (1) ├── integrationTest │ └── groovy // (2) ├── main │ ├── java // (3) └── test └── groovy // (4)
-
包含功能测试的源目录
-
包含集成测试的源目录
-
包含生产源代码的源目录
-
包含单元测试的源目录
笔记
|
这些目录src/integrationTest/groovy 并不src/functionalTest/groovy 基于 Gradle 项目的现有标准约定。您可以自由选择最适合您的项目布局。
|
您可以配置编译和测试执行的源目录。
测试套件插件提供了 DSL 和 API,用于将多组自动化测试建模到基于 JVM 的项目中的测试套件中。为了方便起见,您还可以依赖第三方插件,例如Nebula Facet 插件或TestSets 插件。
建模测试类型
笔记
|
通过孵化JVM 测试套件integrationTest 插件,可以使用
用于建模以下套件的新配置 DSL 。
|
在 Gradle 中,源代码目录使用源集的概念来表示。源集配置为指向一个或多个包含源代码的目录。当您定义源集时,Gradle 会自动为指定目录设置编译任务。
可以使用一行构建脚本代码来创建预配置的源集。源集自动注册配置来定义源集源的依赖关系:
// Define a source set named 'test' for test sources
sourceSets {
test {
java {
srcDirs = ['src/test/java']
}
}
}
// Specify a test implementation dependency on JUnit
dependencies {
testImplementation 'junit:junit:4.12'
}
我们用它来定义integrationTestImplementation
对项目本身的依赖,它代表我们项目的“主要”变体(即编译的插件代码):
val integrationTest by sourceSets.creating
dependencies {
"integrationTestImplementation"(project)
}
def integrationTest = sourceSets.create("integrationTest")
dependencies {
integrationTestImplementation(project)
}
源集负责编译源代码,但不处理字节码的执行。为了执行测试,需要建立相应的Test类型的任务。以下设置显示了集成测试的执行,引用了集成测试源集的类和运行时类路径:
val integrationTestTask = tasks.register<Test>("integrationTest") {
description = "Runs the integration tests."
group = "verification"
testClassesDirs = integrationTest.output.classesDirs
classpath = integrationTest.runtimeClasspath
mustRunAfter(tasks.test)
}
tasks.check {
dependsOn(integrationTestTask)
}
def integrationTestTask = tasks.register("integrationTest", Test) {
description = 'Runs the integration tests.'
group = "verification"
testClassesDirs = integrationTest.output.classesDirs
classpath = integrationTest.runtimeClasspath
mustRunAfter(tasks.named('test'))
}
tasks.named('check') {
dependsOn(integrationTestTask)
}
配置测试框架
下面的代码片段展示了如何使用Spock来实现测试:
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
testImplementation("org.spockframework:spock-core")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
"integrationTestImplementation"(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
"integrationTestImplementation"("org.spockframework:spock-core")
"integrationTestRuntimeOnly"("org.junit.platform:junit-platform-launcher")
"functionalTestImplementation"(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
"functionalTestImplementation"("org.spockframework:spock-core")
"functionalTestRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test>().configureEach {
// Using JUnitPlatform for running tests
useJUnitPlatform()
}
repositories {
mavenCentral()
}
dependencies {
testImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
testImplementation 'org.spockframework:spock-core'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
integrationTestImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
integrationTestImplementation 'org.spockframework:spock-core'
integrationTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
functionalTestImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
functionalTestImplementation 'org.spockframework:spock-core'
functionalTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.withType(Test).configureEach {
// Using JUnitPlatform for running tests
useJUnitPlatform()
}
笔记
|
Spock 是一个基于 Groovy 的 BDD 测试框架,甚至包括用于创建 Stub 和 Mock 的 API。 Gradle 团队更喜欢 Spock 而不是其他选项,因为它的表现力和简洁性。 |
实施自动化测试
本节讨论单元、集成和功能测试的代表性实现示例。所有测试类都基于 Spock 的使用,尽管使代码适应不同的测试框架应该相对容易。
实施单元测试
URL 验证器插件发出 HTTP GET 调用来检查 URL 是否可以成功解析。该方法DefaultHttpCaller.get(String)
负责调用给定的 URL 并返回类型的实例HttpResponse
。HttpResponse
是一个 POJO,包含有关 HTTP 响应代码和消息的信息:
package org.myorg.http;
public class HttpResponse {
private int code;
private String message;
public HttpResponse(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return "HTTP " + code + ", Reason: " + message;
}
}
该类HttpResponse
代表单元测试的良好候选者。它不涉及任何其他类,也不使用 Gradle API。
package org.myorg.http
import spock.lang.Specification
class HttpResponseTest extends Specification {
private static final int OK_HTTP_CODE = 200
private static final String OK_HTTP_MESSAGE = 'OK'
def "can access information"() {
when:
def httpResponse = new HttpResponse(OK_HTTP_CODE, OK_HTTP_MESSAGE)
then:
httpResponse.code == OK_HTTP_CODE
httpResponse.message == OK_HTTP_MESSAGE
}
def "can get String representation"() {
when:
def httpResponse = new HttpResponse(OK_HTTP_CODE, OK_HTTP_MESSAGE)
then:
httpResponse.toString() == "HTTP $OK_HTTP_CODE, Reason: $OK_HTTP_MESSAGE"
}
}
重要的
|
编写单元测试时,测试边界条件和各种形式的无效输入非常重要。尝试从使用 Gradle API 的类中提取尽可能多的逻辑,使其可作为单元测试进行测试。它将产生可维护的代码和更快的测试执行。 |
您可以使用ProjectBuilder类创建Project实例,以便在测试插件实现时使用。
public class GreetingPluginTest {
@Test
public void greeterPluginAddsGreetingTaskToProject() {
Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply("org.example.greeting");
assertTrue(project.getTasks().getByName("hello") instanceof GreetingTask);
}
}
实施集成测试
让我们看一下一个连接到另一个系统的类,即发出 HTTP 调用的代码段。在对类执行测试时DefaultHttpCaller
,运行时环境需要能够连接到互联网:
package org.myorg.http;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class DefaultHttpCaller implements HttpCaller {
@Override
public HttpResponse get(String url) {
try {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
connection.connect();
int code = connection.getResponseCode();
String message = connection.getResponseMessage();
return new HttpResponse(code, message);
} catch (IOException e) {
throw new HttpCallException(String.format("Failed to call URL '%s' via HTTP GET", url), e);
}
}
}
实现集成测试看起来DefaultHttpCaller
与上一节中所示的单元测试没有太大区别:
package org.myorg.http
import spock.lang.Specification
import spock.lang.Subject
class DefaultHttpCallerIntegrationTest extends Specification {
@Subject HttpCaller httpCaller = new DefaultHttpCaller()
def "can make successful HTTP GET call"() {
when:
def httpResponse = httpCaller.get('https://www.google.com/')
then:
httpResponse.code == 200
httpResponse.message == 'OK'
}
def "throws exception when calling unknown host via HTTP GET"() {
when:
httpCaller.get('https://www.wedonotknowyou123.com/')
then:
def t = thrown(HttpCallException)
t.message == "Failed to call URL 'https://www.wedonotknowyou123.com/' via HTTP GET"
t.cause instanceof UnknownHostException
}
}
实施功能测试
功能测试验证插件端到端的正确性。实际上,这意味着应用、配置和执行插件实现的功能。该类UrlVerifierPlugin
公开一个扩展和一个使用最终用户配置的 URL 值的任务实例:
package org.myorg;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.myorg.tasks.UrlVerify;
public class UrlVerifierPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
UrlVerifierExtension extension = project.getExtensions().create("verification", UrlVerifierExtension.class);
UrlVerify verifyUrlTask = project.getTasks().create("verifyUrl", UrlVerify.class);
verifyUrlTask.getUrl().set(extension.getUrl());
}
}
每个 Gradle 插件项目都应该应用插件开发插件来减少样板代码。通过应用插件开发插件,测试源集被预先配置为与 TestKit 一起使用。如果我们想使用自定义源集进行功能测试并保留默认测试源集仅用于单元测试,我们可以配置插件开发插件以在其他地方查找 TestKit 测试。
gradlePlugin {
testSourceSets(functionalTest)
}
gradlePlugin {
testSourceSets(sourceSets.functionalTest)
}
Gradle 插件的功能测试使用 的实例来GradleRunner
执行测试下的构建。
GradleRunner
是TestKit提供的API,内部使用Tooling API来执行构建。
以下示例将插件应用于正在测试的构建脚本,配置扩展并使用任务执行构建verifyUrl
。请参阅TestKit 文档以更熟悉 TestKit 的功能。
package org.myorg
import org.gradle.testkit.runner.GradleRunner
import spock.lang.Specification
import spock.lang.TempDir
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class UrlVerifierPluginFunctionalTest extends Specification {
@TempDir File testProjectDir
File buildFile
def setup() {
buildFile = new File(testProjectDir, 'build.gradle')
buildFile << """
plugins {
id 'org.myorg.url-verifier'
}
"""
}
def "can successfully configure URL through extension and verify it"() {
buildFile << """
verification {
url = 'https://www.google.com/'
}
"""
when:
def result = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments('verifyUrl')
.withPluginClasspath()
.build()
then:
result.output.contains("Successfully resolved URL 'https://www.google.com/'")
result.task(":verifyUrl").outcome == SUCCESS
}
}
IDE集成
TestKit 通过运行特定的 Gradle 任务来确定插件类路径。assemble
即使从 IDE 运行基于 TestKit 的功能测试,您也需要执行任务来最初生成插件类路径或反映对其的更改。
一些 IDE 提供了一个方便的选项来将“测试类路径生成和执行”委托给构建。在 IntelliJ 中,您可以在“首选项...”>“构建、执行、部署”>“构建工具”>“Gradle”>“运行器”>“将 IDE 构建/运行操作委托给 Gradle”下找到此选项。
将插件发布到 Gradle 插件门户
发布插件是使其可供其他人使用的主要方式。虽然您可以发布到私有存储库以限制访问,但发布到Gradle 插件门户可以让世界上的任何人都可以使用您的插件。
本指南向您展示如何使用插件通过方便的 DSLcom.gradle.plugin-publish
将插件发布到Gradle 插件门户。这种方法简化了配置步骤并提供验证检查,以确保您的插件满足 Gradle 插件门户的标准。
先决条件
本教程需要一个现有的 Gradle 插件项目。如果您没有,请使用Greeting 插件示例。
尝试发布此插件将安全地失败并出现权限错误,因此不必担心 Gradle 插件门户会因为一个简单的示例插件而变得混乱。
账户设置
在发布插件之前,您必须在 Gradle 插件门户上创建一个帐户。按照注册页面上的说明创建帐户并从个人资料页面的“API 密钥”选项卡获取 API 密钥。
将 API 密钥存储在 Gradle 配置中(gradle.publish.key 和 gradle.publish.secret),或使用 Seauc Credentials 插件或 Gradle Credentials 插件等插件进行安全管理。
通常的做法是将文本复制并粘贴到$HOME/.gradle/gradle.properties文件中,但您也可以将其放置在任何其他有效位置。该插件所需要的只是在执行适当的插件门户任务时,gradle.publish.key
和可以作为项目属性使用。gradle.publish.secret
如果您担心放置凭据gradle.properties
,请查看Seauc Credentials 插件或Gradle Credentials 插件。
添加插件发布插件
要发布您的插件,请将com.gradle.plugin-publish
插件添加到您的项目build.gradle
或build.gradle.kts
文件中:
plugins {
id("com.gradle.plugin-publish") version "1.2.1"
}
plugins {
id 'com.gradle.plugin-publish' version '1.2.1'
}
最新版本的插件发布插件可以在Gradle 插件门户上找到。
笔记
|
从版本 1.0.0 开始,Plugin Publish Plugin 自动应用 Java Gradle Plugin Development Plugin(协助开发 Gradle 插件)和 Maven Publish Plugin(生成插件发布元数据)。如果使用旧版本的插件发布插件,则必须显式应用这些帮助器插件。 |
配置插件发布插件
com.gradle.plugin-publish
在您的build.gradle
或文件中配置插件build.gradle.kts
。
group = "io.github.johndoe" // (1)
version = "1.0" // (2)
gradlePlugin { // (3)
website = "<substitute your project website>" // (4)
vcsUrl = "<uri to project source repository>" // (5)
// ... // (6)
}
group = 'io.github.johndoe' // (1)
version = '1.0' // (2)
gradlePlugin { // (3)
website = '<substitute your project website>' // (4)
vcsUrl = '<uri to project source repository>' // (5)
// ... // (6)
}
-
确保您的项目有一个
group
集合,用于识别您在 Gradle 插件门户存储库中为插件发布的工件(jar 和元数据),并且描述插件作者或插件所属的组织。 -
设置项目的版本,该版本也将用作插件的版本。
-
使用Java Gradle 插件开发插件
gradlePlugin
提供的块 为您的插件发布配置更多选项。 -
为您的插件项目设置网站。
-
提供源存储库 URI,以便其他想要贡献的人可以找到它。
-
为您要发布的每个插件设置特定属性;请参阅下一节。
使用块定义所有插件的通用属性,例如组、版本、网站和源存储库gradlePlugin{}
:
gradlePlugin { // (1)
// ... // (2)
plugins { // (3)
create("greetingsPlugin") { // (4)
id = "<your plugin identifier>" // (5)
displayName = "<short displayable name for plugin>" // (6)
description = "<human-readable description of what your plugin is about>" // (7)
tags = listOf("tags", "for", "your", "plugins") // (8)
implementationClass = "<your plugin class>"
}
}
}
gradlePlugin { // (1)
// ... // (2)
plugins { // (3)
greetingsPlugin { // (4)
id = '<your plugin identifier>' // (5)
displayName = '<short displayable name for plugin>' // (6)
description = '<human-readable description of what your plugin is about>' // (7)
tags.set(['tags', 'for', 'your', 'plugins']) // (8)
implementationClass = '<your plugin class>'
}
}
}
-
插件特定配置也进入该
gradlePlugin
块。 -
这是我们之前添加全局属性的地方。
-
您发布的每个插件都会有自己的内部块
plugins
。 -
对于您发布的每个插件,插件块的名称必须是唯一的;这是仅由您的构建在本地使用的属性,不会成为发布的一部分。
-
设置插件的唯一性
id
,因为它将在出版物中标识。 -
以人类可读的形式设置插件名称。
-
设置要在门户上显示的说明。它为想要使用您的插件的人提供有用的信息。
-
指定您的插件涵盖的类别。它使插件更有可能被需要其功能的人发现。
例如,考虑已发布到 Gradle 插件门户的GradleTest 插件的配置。
gradlePlugin {
website = "https://github.com/ysb33r/gradleTest"
vcsUrl = "https://github.com/ysb33r/gradleTest.git"
plugins {
create("gradletestPlugin") {
id = "org.ysb33r.gradletest"
displayName = "Plugin for compatibility testing of Gradle plugins"
description = "A plugin that helps you test your plugin against a variety of Gradle versions"
tags = listOf("testing", "integrationTesting", "compatibility")
implementationClass = "org.ysb33r.gradle.gradletest.GradleTestPlugin"
}
}
}
gradlePlugin {
website = 'https://github.com/ysb33r/gradleTest'
vcsUrl = 'https://github.com/ysb33r/gradleTest.git'
plugins {
gradletestPlugin {
id = 'org.ysb33r.gradletest'
displayName = 'Plugin for compatibility testing of Gradle plugins'
description = 'A plugin that helps you test your plugin against a variety of Gradle versions'
tags.addAll('testing', 'integrationTesting', 'compatibility')
implementationClass = 'org.ysb33r.gradle.gradletest.GradleTestPlugin'
}
}
}
如果您浏览 Gradle 插件门户上GradleTest 插件的关联页面,您将看到指定元数据的显示方式。
来源和 Javadoc
插件发布插件会自动生成并发布Javadoc,以及插件发布的源 JAR。
影子依赖
从 Plugin Publish Plugin 版本 1.0.0 开始,隐藏插件的依赖项(即,将其发布为 fat jar)已自动实现。要启用它,所需要做的就是com.github.johnrengelman.shadow
在您的构建中应用该插件。
发布插件
如果您有兴趣发布您的插件以供更广泛的 Gradle 社区使用,您可以将其发布到Gradle Plugin Portal。该站点提供搜索和收集有关 Gradle 社区贡献的插件信息的功能。请参阅有关在本网站上提供您的插件的相应部分。
本地发布
要检查已发布插件的工件外观或仅在本地或公司内部使用它,您可以将其发布到任何 Maven 存储库,包括本地文件夹。您只需要配置用于发布的存储库。然后,您可以运行publish
任务将插件发布到您定义的所有存储库(但不是 Gradle 插件门户)。
publishing {
repositories {
maven {
name = "localPluginRepository"
url = uri("../local-plugin-repository")
}
}
}
publishing {
repositories {
maven {
name = 'localPluginRepository'
url = '../local-plugin-repository'
}
}
}
要在另一个版本中使用该存储库,请将其添加到文件中块的存储库pluginManagement {}
settings.gradle(.kts)
中。
发布到插件门户
使用publishPlugin
任务发布插件:
$ ./gradlew publishPlugins
您可以在发布之前使用该标志验证您的插件--validate-only
:
$ ./gradlew publishPlugins --validate-only
如果您尚未配置gradle.properties
Gradle 插件门户,您可以在命令行上指定它们:
$ ./gradlew publishPlugins -Pgradle.publish.key=<key> -Pgradle.publish.secret=<secret>
笔记
|
如果您尝试使用本节中使用的 ID 发布示例 Greeting 插件,您将遇到权限失败。这是预期的,并确保门户不会被多个实验性和重复的问候类型插件淹没。 |
批准后,您的插件将在 Gradle 插件门户上提供,供其他人发现和使用。
使用已发布的插件
成功发布插件后,它不会立即出现在门户上。它还需要通过审批流程,对于插件的初始版本来说,这是手动的并且相对较慢,但对于后续版本来说是全自动的。有关更多详细信息,请参阅此处。
一旦您的插件获得批准,您可以在https://plugins.gradle.org/plugin/<your-plugin-id>形式的 URL 中找到其使用说明。例如,Greeting 插件示例已位于门户网站https://plugins.gradle.org/plugin/org.example.greeting上。
没有 Gradle 插件门户发布的插件
如果您的插件是在没有使用Java Gradle Plugin Development Plugin 的情况下发布的,则该发布将缺少Plugin Marker Artifact ,插件 DSL需要它来定位插件。在这种情况下,解决另一个项目中的插件的推荐方法是将一个resolutionStrategy
部分添加到pluginManagement {}
项目设置文件的块中,如下所示。
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == "org.example") {
useModule("org.example:custom-plugin:${requested.version}")
}
}
}
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == 'org.example') {
useModule("org.example:custom-plugin:${requested.version}")
}
}
}
最佳实践
组织 Gradle 项目
每个软件项目的源代码和构建逻辑都应该以有意义的方式组织。本页列出了可实现可读、可维护项目的最佳实践。以下各节还介绍了常见问题以及如何避免这些问题。
单独的特定于语言的源文件
Gradle 的语言插件建立了发现和编译源代码的约定。例如,应用Java插件的项目会自动编译目录中的代码src/main/java
。其他语言插件遵循相同的模式。目录路径的最后部分通常指示源文件的预期语言。
有些编译器能够在同一源目录中交叉编译多种语言。 Groovy 编译器可以处理混合位于src/main/groovy
. Gradle 建议您根据源语言将源代码放置在目录中,因为构建的性能更高,并且用户和构建都可以做出更强有力的假设。
以下源代码树包含 Java 和 Kotlin 源文件。 Java 源文件位于src/main/java
,而 Kotlin 源文件位于src/main/kotlin
.
.
├── build.gradle.kts
└── src
└── main
├── java
│ └── HelloWorld.java
└── kotlin
└── Utils.kt
.
├── build.gradle
└── src
└── main
├── java
│ └── HelloWorld.java
└── kotlin
└── Utils.kt
每个测试类型单独的源文件
项目定义和执行不同类型的测试是很常见的,例如单元测试、集成测试、功能测试或冒烟测试。最佳情况下,每种测试类型的测试源代码应存储在专用的源目录中。分离的测试源代码对可维护性和关注点分离具有积极影响,因为您可以彼此独立地运行测试类型。
查看演示 如何将单独的集成测试配置添加到基于 Java 的项目的示例。
尽可能使用标准约定
-
它将目录定义
src/main/java
为编译的默认源目录。 -
已编译源代码和其他工件(如 JAR 文件)的输出目录是
build
.
通过坚持默认约定,项目的新开发人员可以立即知道如何找到解决办法。虽然这些约定可以重新配置,但这使得构建脚本用户和作者来管理构建逻辑及其结果变得更加困难。尝试尽可能坚持默认约定,除非您需要适应遗留项目的布局。请参阅相关插件的参考页面以了解其默认约定。
始终定义一个设置文件
每次调用构建时,Gradle 都会尝试定位settings.gradle
(Groovy DSL) 或(Kotlin DSL) 文件。settings.gradle.kts
为此,运行时将目录树的层次结构向上移动到根目录。一旦找到设置文件,算法就会停止搜索。
始终将 a 添加settings.gradle
到构建的根目录以避免初始性能影响。该文件可以为空,也可以定义所需的项目名称。
多项目构建必须settings.gradle(.kts)
在多项目层次结构的根项目中有一个文件。这是必需的,因为设置文件定义了哪些项目正在参与多项目构建。除了定义包含的项目之外,您可能还需要它将库添加到构建脚本类路径中。
以下示例显示了标准 Gradle 项目布局:
.
├── settings.gradle.kts
├── subproject-one
│ └── build.gradle.kts
└── subproject-two
└── build.gradle.kts
.
├── settings.gradle
├── subproject-one
│ └── build.gradle
└── subproject-two
└── build.gradle
用于buildSrc
抽象命令式逻辑
复杂的构建逻辑通常适合封装为自定义任务或二进制插件。自定义任务和插件实现不应存在于构建脚本中。buildSrc
只要代码不需要在多个独立项目之间共享,就可以非常方便地用于此目的。
该目录buildSrc
被视为包含的构建。发现该目录后,Gradle 会自动编译和测试此代码,并将其放入构建脚本的类路径中。对于多项目构建,只能有一个buildSrc
目录,该目录必须位于根项目目录中。
buildSrc
应该优先于脚本插件,因为它更容易维护、重构和测试代码。
buildSrc
使用适用于 Java 和 Groovy 项目的相同源代码约定。它还提供对 Gradle API 的直接访问。额外的依赖项可以在专用build.gradle
的buildSrc
.
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.13'
}
一个典型的项目包括buildSrc
以下布局。下面的任何代码buildSrc
都应该使用与应用程序代码类似的包。或者,如果需要额外的配置(例如应用插件或声明依赖项),该buildSrc
目录可以托管构建脚本。
.
├── buildSrc
│ ├── build.gradle.kts
│ └── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── enterprise
│ │ ├── Deploy.java
│ │ └── DeploymentPlugin.java
│ └── test
│ └── java
│ └── com
│ └── enterprise
│ └── DeploymentPluginTest.java
├── settings.gradle.kts
├── subproject-one
│ └── build.gradle.kts
└── subproject-two
└── build.gradle.kts
.
├── buildSrc
│ ├── build.gradle
│ └── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── enterprise
│ │ ├── Deploy.java
│ │ └── DeploymentPlugin.java
│ └── test
│ └── java
│ └── com
│ └── enterprise
│ └── DeploymentPluginTest.java
├── settings.gradle
├── subproject-one
│ └── build.gradle
└── subproject-two
└── build.gradle
笔记
|
更改 因此,当进行小的增量更改时, |
gradle.properties
在文件中声明属性
gradle.properties
在 Gradle 中,属性可以在构建脚本、文件中或命令行上的参数中定义。
对于临时场景,在命令行上声明属性是很常见的。例如,您可能希望传入一个特定的属性值来控制构建的这一次调用的运行时行为。构建脚本中的属性很容易成为维护难题,并使构建脚本逻辑变得复杂。这gradle.properties
有助于将属性与构建脚本分开,应该将其作为可行的选项进行探索。这是放置控制构建环境的属性的好位置。
典型的项目设置将该gradle.properties
文件放置在构建的根目录中。或者,GRADLE_USER_HOME
如果您希望该文件应用于计算机上的所有版本,则该文件也可以位于该目录中。
.
├── gradle.properties
└── settings.gradle.kts
├── subproject-a
│ └── build.gradle.kts
└── subproject-b
└── build.gradle.kts
.
├── gradle.properties
└── settings.gradle
├── subproject-a
│ └── build.gradle
└── subproject-b
└── build.gradle
避免任务输出重叠
任务应该定义输入和输出以获得增量构建功能的性能优势。声明任务的输出时,请确保用于写入输出的目录在项目中的所有任务中是唯一的。
混合或覆盖不同任务生成的输出文件会损害最新检查,导致构建速度变慢。反过来,这些文件系统更改可能会阻止 Gradle 的构建缓存正确识别和缓存本来可缓存的任务。
使用自定义 Gradle 发行版标准化构建
企业通常希望通过定义通用约定或规则来标准化组织中所有项目的构建平台。您可以借助初始化脚本来实现这一点。 初始化脚本使得在单台机器上的所有项目中应用构建逻辑变得非常容易。例如,声明内部存储库及其凭据。
该方法有一些缺点。首先,您必须与公司的所有开发人员沟通设置过程。此外,统一更新初始化脚本逻辑可能具有挑战性。
自定义 Gradle 发行版是解决这个问题的实用方法。自定义 Gradle 发行版由标准 Gradle 发行版以及一个或多个自定义初始化脚本组成。初始化脚本与发行版捆绑在一起,并在每次运行构建时应用。开发人员只需将签入的Wrapper文件指向自定义 Gradle 发行版的 URL。
自定义 Gradle 发行版还可能gradle.properties
在发行版的根目录中包含一个文件,该文件提供了一组组织范围内的属性来控制构建环境。
以下步骤是创建自定义 Gradle 发行版的典型步骤:
-
实现下载和重新打包 Gradle 发行版的逻辑。
-
使用所需逻辑定义一个或多个初始化脚本。
-
将初始化脚本与 Gradle 发行版捆绑在一起。
-
将 Gradle 分发存档上传到 HTTP 服务器。
-
更改所有项目的 Wrapper 文件以指向自定义 Gradle 发行版的 URL。
plugins {
id 'base'
}
// This is defined in buildSrc
import org.gradle.distribution.DownloadGradle
version = '0.1'
tasks.register('downloadGradle', DownloadGradle) {
description = 'Downloads the Gradle distribution with a given version.'
gradleVersion = '4.6'
}
tasks.register('createCustomGradleDistribution', Zip) {
description = 'Builds custom Gradle distribution and bundles initialization scripts.'
dependsOn downloadGradle
def projectVersion = project.version
archiveFileName = downloadGradle.gradleVersion.map { gradleVersion ->
"mycompany-gradle-${gradleVersion}-${projectVersion}-bin.zip"
}
from zipTree(downloadGradle.destinationFile)
from('src/init.d') {
into "${downloadGradle.distributionNameBase.get()}/init.d"
}
}
编写可维护构建的最佳实践
Gradle 拥有丰富的 API,提供多种创建构建逻辑的方法。相关的灵活性很容易导致不必要的复杂构建,而自定义代码通常直接添加到构建脚本中。在本章中,我们将介绍几种最佳实践,帮助您开发易于使用、富有表现力且可维护的构建。
笔记
|
如果您感兴趣的话, 第三方Gradle lint 插件有助于在构建脚本中强制执行所需的代码样式。 |
避免在脚本中使用命令式逻辑
Gradle 运行时不强制构建逻辑的特定样式。正是出于这个原因,很容易最终得到一个将声明性 DSL 元素与命令式过程代码混合在一起的构建脚本。我们来谈谈一些具体的例子。
-
声明性代码:内置的、与语言无关的 DSL 元素(例如Project.dependency{}或Project.repositories{})或插件公开的 DSL
-
命令式代码:条件逻辑或非常复杂的任务操作实现
每个构建脚本的最终目标应该是仅包含声明性语言元素,这使得代码更易于理解和维护。命令式逻辑应该存在于二进制插件中,然后应用于构建脚本。作为副产品,如果您将工件发布到二进制存储库,您将自动使您的团队能够在其他项目中重用插件逻辑。
以下示例构建显示了直接在构建脚本中使用条件逻辑的反面示例。虽然此代码片段很小,但很容易想象一个使用大量过程语句的完整构建脚本及其对可读性和可维护性的影响。通过将代码移动到一个类中,也可以单独进行测试。
if (project.findProperty("releaseEngineer") != null) {
tasks.register("release") {
doLast {
logger.quiet("Releasing to production...")
// release the artifact to production
}
}
}
if (project.findProperty('releaseEngineer') != null) {
tasks.register('release') {
doLast {
logger.quiet 'Releasing to production...'
// release the artifact to production
}
}
}
让我们将构建脚本与作为二进制插件实现的相同逻辑进行比较。该代码乍一看可能看起来更复杂,但显然看起来更像典型的应用程序代码。这个特定的插件类位于buildSrc
目录中,这使得构建脚本可以自动使用它。
package com.enterprise;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.tasks.TaskProvider;
public class ReleasePlugin implements Plugin<Project> {
private static final String RELEASE_ENG_ROLE_PROP = "releaseEngineer";
private static final String RELEASE_TASK_NAME = "release";
@Override
public void apply(Project project) {
if (project.findProperty(RELEASE_ENG_ROLE_PROP) != null) {
Task task = project.getTasks().create(RELEASE_TASK_NAME);
task.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
task.getLogger().quiet("Releasing to production...");
// release the artifact to production
}
});
}
}
}
现在构建逻辑已转换为插件,您可以将其应用到构建脚本中。构建脚本已从 8 行代码缩减为一行。
plugins {
id("com.enterprise.release")
}
plugins {
id 'com.enterprise.release'
}
避免使用内部 Gradle API
当 Gradle 或插件发生更改时,在插件和构建脚本中使用 Gradle 内部 API 可能会破坏构建。
Gradle 公共 API 定义
和
Kotlin DSL API 定义中列出了以下包
,internal
名称中带有 的任何子包除外。
org.gradle org.gradle.api.* org.gradle.authentication.* org.gradle.build.* org.gradle.buildinit.* org.gradle.caching.* org.gradle.concurrent.* org.gradle.deployment.* org.gradle.external.javadoc.* org.gradle.ide.* org.gradle.ivy.* org.gradle.jvm.* org.gradle.language.* org.gradle.maven.* org.gradle.nativeplatform.* org.gradle.normalization.* org.gradle.platform.* org.gradle.plugin.devel.* org.gradle.plugin.use org.gradle.plugin.management org.gradle.plugins.* org.gradle.process.* org.gradle.testfixtures.* org.gradle.testing.jacoco.* org.gradle.tooling.* org.gradle.swiftpm.* org.gradle.model.* org.gradle.testkit.* org.gradle.testing.* org.gradle.vcs.* org.gradle.work.* org.gradle.workers.* org.gradle.util.*
org.gradle.kotlin.dsl org.gradle.kotlin.dsl.precompile
常用内部 API 的替代方案
要为您的自定义任务提供嵌套 DSL,请勿使用org.gradle.internal.reflect.Instantiator
;请改用ObjectFactory。阅读有关延迟配置的章节也可能会有所帮助。
代替org.gradle.internal.os.OperatingSystem
,使用另一种方法来检测操作系统,例如Apache commons-lang SystemUtils或System.getProperty("os.name")
.
使用其他集合或 I/O 框架代替org.gradle.util.CollectionUtils
、org.gradle.util.internal.GFileUtils
和 下的其他类org.gradle.util.*
。
声明任务时遵循约定
任务 API 为构建作者在构建脚本中声明任务提供了很大的灵活性。为了获得最佳的可读性和可维护性,请遵循以下规则:
-
任务类型应该是任务名称后面括号内的唯一键值对。
-
其他配置应在任务的配置块内完成。
-
声明任务时添加的任务操作只能使用方法Task.doFirst{}或Task.doLast{}声明。
-
声明临时任务(没有显式类型的任务)时,如果您仅声明单个操作,则应使用Task.doLast{} 。
-
任务应该定义组和描述。
import com.enterprise.DocsGenerate
tasks.register<DocsGenerate>("generateHtmlDocs") {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates the HTML documentation for this project."
title = "Project docs"
outputDir = layout.buildDirectory.dir("docs")
}
tasks.register("allDocs") {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates all documentation for this project."
dependsOn("generateHtmlDocs")
doLast {
logger.quiet("Generating all documentation...")
}
}
import com.enterprise.DocsGenerate
def generateHtmlDocs = tasks.register('generateHtmlDocs', DocsGenerate) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = 'Generates the HTML documentation for this project.'
title = 'Project docs'
outputDir = layout.buildDirectory.dir('docs')
}
tasks.register('allDocs') {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = 'Generates all documentation for this project.'
dependsOn generateHtmlDocs
doLast {
logger.quiet('Generating all documentation...')
}
}
提高任务的可发现性
即使是构建的新用户也应该能够快速、轻松地找到关键信息。在 Gradle 中,您可以为构建的任何任务声明一个组和描述。任务报告使用分配的值来组织和呈现任务,以便于发现。分配组和描述对于您希望构建用户调用的任何任务最有帮助。
该示例任务generateDocs
以 HTML 页面的形式生成项目文档。任务应该组织在桶下面Documentation
。描述应表达其意图。
tasks.register("generateDocs") {
group = "Documentation"
description = "Generates the HTML documentation for this project."
doLast {
// action implementation
}
}
tasks.register('generateDocs') {
group = 'Documentation'
description = 'Generates the HTML documentation for this project.'
doLast {
// action implementation
}
}
任务报告的输出反映了分配的值。
> gradle tasks > Task :tasks Documentation tasks ------------------- generateDocs - Generates the HTML documentation for this project.
最小化配置阶段执行的逻辑
对于每个构建脚本开发人员来说,了解构建生命周期的不同阶段及其对构建逻辑的性能和评估顺序的影响非常重要。在配置阶段,应该配置项目及其域对象,而执行阶段仅执行命令行上请求的任务的操作及其依赖项。请注意,任何不属于任务操作的代码都将在每次构建运行时执行。构建扫描可以帮助您确定每个生命周期阶段所花费的时间。它是诊断常见性能问题的宝贵工具。
让我们考虑一下上述反模式的以下咒语。在构建脚本中,您可以看到分配给配置的依赖项printArtifactNames
是在任务操作之外解析的。
dependencies {
implementation("log4j:log4j:1.2.17")
}
tasks.register("printArtifactNames") {
// always executed
val libraryNames = configurations.compileClasspath.get().map { it.name }
doLast {
logger.quiet(libraryNames.joinToString())
}
}
dependencies {
implementation 'log4j:log4j:1.2.17'
}
tasks.register('printArtifactNames') {
// always executed
def libraryNames = configurations.compileClasspath.collect { it.name }
doLast {
logger.quiet libraryNames
}
}
用于解决依赖关系的代码应移至任务操作中,以避免在实际需要依赖关系之前解决依赖关系对性能造成影响。
dependencies {
implementation("log4j:log4j:1.2.17")
}
tasks.register("printArtifactNames") {
val compileClasspath: FileCollection = configurations.compileClasspath.get()
doLast {
val libraryNames = compileClasspath.map { it.name }
logger.quiet(libraryNames.joinToString())
}
}
dependencies {
implementation 'log4j:log4j:1.2.17'
}
tasks.register('printArtifactNames') {
FileCollection compileClasspath = configurations.compileClasspath
doLast {
def libraryNames = compileClasspath.collect { it.name }
logger.quiet libraryNames
}
}
避免使用GradleBuild
任务类型
GradleBuild任务类型允许构建脚本定义调用另一个 Gradle 构建的任务。通常不鼓励使用这种类型。在某些特殊情况下,调用的构建不会公开与命令行或工具 API 相同的运行时行为,从而导致意外结果。
通常,有更好的方法来对需求进行建模。适当的方法取决于当前的问题。这里有一些选项:
避免项目间配置
Gradle 不限制构建脚本作者在多项目构建中从一个项目进入另一个项目的域模型。强耦合的项目会损害构建执行性能以及代码的可读性和可维护性。
应避免以下做法:
-
通过Task.dependsOn(java.lang.Object...)显式依赖于另一个项目的任务。
-
从另一个项目设置属性值或调用域对象的方法。
-
使用GradleBuild执行构建的另一部分。
-
声明不必要的项目依赖关系。
外部化并加密您的密码
大多数构建需要使用一个或多个密码。这种需要的原因可能有所不同。某些构建需要密码才能将工件发布到安全的二进制存储库,其他构建需要密码才能下载二进制文件。密码应始终安全,以防止欺诈。在任何情况下都不应将密码以纯文本形式添加到构建脚本中或在gradle.properties
项目目录的文件中声明它。这些文件通常位于版本控制存储库中,任何有权访问它的人都可以查看。
密码和任何其他敏感数据应保存在版本控制项目文件的外部。 Gradle 公开了一个 API,用于在ProviderFactory
和Artifact Repositories中提供凭证
,允许
在构建需要时使用Gradle 属性提供凭证值。这样,凭据可以存储在gradle.properties
驻留在用户主目录中的文件中,或者使用命令行参数或环境变量注入到构建中。
如果您将敏感凭据存储在用户 home 中gradle.properties
,请考虑对其进行加密。目前 Gradle 不提供用于加密、存储和访问密码的内置机制。解决这个问题的一个很好的解决方案是Gradle Credentials 插件。
不要预期配置创建
Gradle 将使用“如果需要则检查”策略创建某些配置,例如default
或。archives
这意味着它只会创建这些配置(如果它们尚不存在)。
您不应该自己创建这些配置。诸如此类的名称以及与源集关联的配置的名称应被视为隐式“保留”。保留名称的确切列表取决于应用的插件以及构建的配置方式。
这种情况将通过以下弃用警告来宣布:
Configuration customCompileClasspath already exists with permitted usage(s):
Consumable - this configuration can be selected by another project as a dependency
Resolvable - this configuration can be resolved by this project to a set of files
Declarable - this configuration can have dependencies added to it
Yet Gradle expected to create it with the usage(s):
Resolvable - this configuration can be resolved by this project to a set of files
然后,Gradle 将尝试改变允许的用法以匹配预期的用法,并发出第二个警告:
Gradle will mutate the usage of this configuration to match the expected usage. This may cause unexpected behavior. Creating configurations with reserved names has been deprecated. This is scheduled to be removed in Gradle 9.0. Create source sets prior to creating or accessing the configurations associated with them.
某些配置可能会锁定其用途以防止突变。在这种情况下,您的构建将失败,并且此警告之后将立即出现带有以下消息的异常:
Gradle cannot mutate the usage of configuration 'customCompileClasspath' because it is locked.
如果您遇到此错误,您必须:
-
更改配置的名称以避免冲突。
-
如果无法更改名称,请确保配置允许的用途(可消耗的、可解析的、可声明的)与 Gradle 的期望一致。
作为最佳实践,您不应该“预期”配置创建 - 让 Gradle 首先创建配置,然后调整它。或者,如果可能,请在看到此警告时重命名自定义配置,使用不冲突的名称。
其他主题
Gradle 管理的目录
Gradle 使用两个主要目录来执行和管理其工作:Gradle 用户主目录和项目根目录。
Gradle 用户主目录
默认情况下,Gradle 用户主页(~/.gradle
或C:\Users\<USERNAME>\.gradle
)存储全局配置属性、初始化脚本、缓存和日志文件。
可以通过环境变量来设置GRADLE_USER_HOME
。
提示
|
GRADLE_HOME 不要与Gradle 的可选安装目录
混淆。 |
其结构大致如下:
├── caches // (1) │ ├── 4.8 // (2) │ ├── 4.9 // (2) │ ├── ⋮ │ ├── jars-3 // (3) │ └── modules-2 // (3) ├── daemon // (4) │ ├── ⋮ │ ├── 4.8 │ └── 4.9 ├── init.d // (5) │ └── my-setup.gradle ├── jdks // (6) │ ├── ⋮ │ └── jdk-14.0.2+12 ├── wrapper │ └── dists // (7) │ ├── ⋮ │ ├── gradle-4.8-bin │ ├── gradle-4.9-all │ └── gradle-4.9-bin └── gradle.properties // (8)
-
全局缓存目录(用于所有非项目特定的内容)。
-
版本特定的缓存(例如,支持增量构建)。
-
共享缓存(例如,用于依赖项的工件)。
-
Gradle Daemon的注册表和日志。
-
全局初始化脚本。
-
通过工具链支持下载的 JDK 。
-
由Gradle Wrapper下载的发行版。
-
全局Gradle 配置属性。
清理缓存和分发
Gradle 会自动清理其用户主目录。
默认情况下,当 Gradle 守护程序停止或关闭时,清理工作在后台运行。
如果使用--no-daemon
,它会在构建会话后在前台运行。
定期应用以下清理策略(默认情况下,每 24 小时一次):
-
检查所有目录中特定于版本的缓存
caches/<GRADLE_VERSION>/
是否仍在使用。否则,发布版本的目录将在 30 天不活动后被删除,快照版本的目录将在 7 天后被删除。
-
caches/
检查(例如)中的共享缓存jars-*
是否仍在使用。如果没有 Gradle 版本仍在使用它们,它们将被删除。
-
检查当前 Gradle 版本
caches/
(例如jars-3
或)中使用的共享缓存中的文件的上次访问时间。modules-2
根据文件是否可以在本地重新创建或从远程存储库下载,该文件将分别在 7 天或 30 天后删除。
-
检查Gradle 发行版
wrapper/dists/
是否仍在使用,即是否存在相应的特定于版本的缓存目录。未使用的发行版将被删除。
配置缓存和分发的清理
可以配置各种缓存的保留期限。
缓存分为四类:
-
已发布的包装器发行版:与已发行版本相对应的发行版和相关版本特定缓存(例如,
4.6.2
或8.0
)。未使用版本的默认保留期为 30 天。
-
快照包装器发行版:与快照版本相对应的发行版和相关版本特定缓存(例如
7.6-20221130141522+0000
)。未使用版本的默认保留期为 7 天。
-
下载的资源:从远程存储库下载的共享缓存(例如,缓存的依赖项)。
未使用资源的默认保留期为 30 天。
-
创建的资源: Gradle 在构建期间创建的共享缓存(例如,工件转换)。
未使用资源的默认保留期为 7 天。
每个类别的保留期可以通过 Gradle 用户主页中的 init 脚本独立配置:
beforeSettings {
caches {
releasedWrappers.setRemoveUnusedEntriesAfterDays(45)
snapshotWrappers.setRemoveUnusedEntriesAfterDays(10)
downloadedResources.setRemoveUnusedEntriesAfterDays(45)
createdResources.setRemoveUnusedEntriesAfterDays(10)
}
}
beforeSettings { settings ->
settings.caches {
releasedWrappers.removeUnusedEntriesAfterDays = 45
snapshotWrappers.removeUnusedEntriesAfterDays = 10
downloadedResources.removeUnusedEntriesAfterDays = 45
createdResources.removeUnusedEntriesAfterDays = 10
}
}
调用缓存清理的频率也是可配置的。
有三种可能的设置:
-
默认:在后台定期执行清理(目前每 24 小时一次)。
-
禁用:切勿清理 Gradle 用户主页。
这在 Gradle User Home 是短暂的或者需要延迟清理直到明确的点的情况下非常有用。
-
始终:在每个构建会话结束时执行清理。
这在需要确保在继续之前进行清理的情况下非常有用。
但是,这会在构建期间(而不是在后台)执行缓存清理,这可能会很昂贵,因此仅应在必要时使用此选项。
要禁用缓存清理:
beforeSettings {
caches {
cleanup = Cleanup.DISABLED
}
}
beforeSettings { settings ->
settings.caches {
cleanup = Cleanup.DISABLED
}
}
笔记
|
缓存清理设置只能通过 init 脚本进行配置,并且应放置init.d 在 Gradle User Home 的目录下。这有效地将缓存清理的配置耦合到这些设置所应用的 Gradle 用户主页,并限制了将来自不同项目的不同冲突设置应用于同一目录的可能性。
|
多个版本的 Gradle 共享一个 Gradle 用户主页
在多个版本的 Gradle 之间共享一个 Gradle 用户主页是很常见的。
如上所述,Gradle User Home 中的缓存是特定于版本的。不同版本的 Gradle 将仅对与每个版本关联的特定于版本的缓存执行维护。
另一方面,一些缓存在版本之间共享(例如,依赖性工件缓存或工件变换缓存)。
从 Gradle 版本 8.0 开始,可以将缓存清理设置配置为自定义保留期。但是,旧版本有固定的保留期(7 或 30 天,具体取决于缓存)。这些共享缓存可以通过具有不同设置的 Gradle 版本来访问,以保留缓存工件。
这意味着:
-
如果未自定义保留期,则所有执行清理的版本将具有相同的保留期。多个版本共享一个 Gradle User Home 不会有任何影响。
-
如果为大于或等于版本 8.0 的 Gradle 版本自定义保留期以使用比之前固定期限更短的保留期,也不会产生任何影响。
了解这些设置的 Gradle 版本将比之前固定的保留期更早地清理工件,而旧版本实际上不会参与共享缓存的清理。
-
如果为大于或等于版本 8.0 的 Gradle 版本自定义保留期以使用比之前固定期限更长的保留期,则旧版本的 Gradle 可能会比配置的更早清理共享缓存。
在这种情况下,如果需要为新版本维护这些共享缓存条目更长的保留期,它们将无法与旧版本共享 Gradle 用户主页。他们将需要使用单独的目录。
与 8.0 之前的 Gradle 版本共享 Gradle 用户主页时的另一个考虑因素是,用于配置缓存保留设置的 DSL 元素在早期版本中不可用,因此必须在版本之间共享的任何初始化脚本中考虑到这一点。这可以通过有条件地应用版本兼容的脚本来轻松处理。
笔记
|
版本兼容的脚本应该驻留在init.d 目录之外的其他位置(例如子目录),因此不会自动应用它。
|
要以版本安全的方式配置缓存清理:
if (GradleVersion.current() >= GradleVersion.version("8.0")) {
apply(from = "gradle8/cache-settings.gradle.kts")
}
if (GradleVersion.current() >= GradleVersion.version('8.0')) {
apply from: "gradle8/cache-settings.gradle"
}
版本兼容的缓存配置脚本:
beforeSettings {
caches {
releasedWrappers { setRemoveUnusedEntriesAfterDays(45) }
snapshotWrappers { setRemoveUnusedEntriesAfterDays(10) }
downloadedResources { setRemoveUnusedEntriesAfterDays(45) }
createdResources { setRemoveUnusedEntriesAfterDays(10) }
}
}
beforeSettings { settings ->
settings.caches {
releasedWrappers.removeUnusedEntriesAfterDays = 45
snapshotWrappers.removeUnusedEntriesAfterDays = 10
downloadedResources.removeUnusedEntriesAfterDays = 45
createdResources.removeUnusedEntriesAfterDays = 10
}
}
缓存标记
从 Gradle 版本 8.1 开始,Gradle 支持使用CACHEDIR.TAG
文件标记缓存。
它遵循缓存目录标记规范中描述的格式。该文件的目的是让工具能够识别不需要搜索或备份的目录。
默认情况下, Gradle 用户主页中的目录caches
、wrapper/dists
、daemon
、 和都标有此文件。jdks
配置缓存标记
缓存标记功能可以通过 Gradle 用户主页中的 init 脚本进行配置:
beforeSettings {
caches {
// Disable cache marking for all caches
markingStrategy = MarkingStrategy.NONE
}
}
beforeSettings { settings ->
settings.caches {
// Disable cache marking for all caches
markingStrategy = MarkingStrategy.NONE
}
}
笔记
|
缓存标记设置只能通过 init 脚本进行配置,并且应放置init.d 在 Gradle User Home 的目录下。这有效地将缓存标记的配置耦合到应用这些设置的 Gradle 用户主页,并限制将来自不同项目的不同冲突设置应用于同一目录的可能性。
|
项目根目录
项目根目录包含项目中的所有源文件。
它还包含 Gradle 生成的文件和目录,例如.gradle
和build
。
虽然前者通常会签入源代码管理,但后者是 Gradle 用于支持增量构建等功能的临时文件。
典型项目根目录的剖析如下:
├── .gradle // (1) │ ├── 4.8 // (2) │ ├── 4.9 // (2) │ └── ⋮ ├── build // (3) ├── gradle │ └── wrapper // (4) ├── gradle.properties // (5) ├── gradlew // (6) ├── gradlew.bat // (6) ├── settings.gradle.kts // (7) ├── subproject-one // (8) | └── build.gradle.kts // (9) ├── subproject-two // (8) | └── build.gradle.kts // (9) └── ⋮
-
Gradle 生成的项目特定的缓存目录。
-
版本特定的缓存(例如,支持增量构建)。
-
该项目的构建目录,Gradle 在其中生成所有构建工件。
-
包含Gradle Wrapper的 JAR 文件和配置。
-
项目特定的Gradle 配置属性。
-
使用Gradle Wrapper执行构建的脚本。
-
定义子项目列表的项目设置文件。
-
通常,一个项目被组织成一个或多个子项目。
-
每个子项目都有自己的 Gradle 构建脚本。
项目缓存清理
从版本 4.10 开始,Gradle 会自动清理项目特定的缓存目录。
构建项目后,.gradle/8.7/
会定期(最多每 24 小时)检查 中特定于版本的缓存目录,以确定它们是否仍在使用。如果 7 天未使用,它们将被删除。
下一步: 了解 Gradle 构建生命周期>>
使用文件
几乎每个 Gradle 构建都以某种方式与文件交互:想想源文件、文件依赖项、报告等。这就是 Gradle 提供全面 API 的原因,可以让您轻松执行所需的文件操作。
API 有两个部分:
-
指定要处理的文件和目录
-
指定如何处理它们
复制单个文件
您可以通过创建 Gradle 内置复制任务的实例并使用文件的位置和要放置文件的位置来配置它来复制文件。此示例模拟将生成的报告复制到将打包到存档中的目录,例如 ZIP 或 TAR:
tasks.register<Copy>("copyReport") {
from(layout.buildDirectory.file("reports/my-report.pdf"))
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyReport', Copy) {
from layout.buildDirectory.file("reports/my-report.pdf")
into layout.buildDirectory.dir("toArchive")
}
ProjectLayout类用于查找相对于当前项目的文件或目录路径。这是使构建脚本无论项目路径如何都能正常工作的常用方法。然后,文件和目录路径用于指定使用Copy.from(java.lang.Object…)复制哪个文件以及使用Copy.into(java.lang.Object)将其复制到哪个目录。
尽管硬编码路径可以提供简单的示例,但它们也会使构建变得脆弱。最好使用可靠的单一事实来源,例如任务或共享项目属性。在下面的修改示例中,我们使用在其他地方定义的报告任务,该任务将报告的位置存储在其outputFile
属性中:
tasks.register<Copy>("copyReport2") {
from(myReportTask.flatMap { it.outputFile })
into(archiveReportsTask.flatMap { it.dirToArchive })
}
tasks.register('copyReport2', Copy) {
from myReportTask.outputFile
into archiveReportsTask.dirToArchive
}
我们还假设报告将由 存档archiveReportsTask
,这为我们提供了将存档的目录,以及我们想要放置报告副本的位置。
复制多个文件
您可以通过提供多个参数来轻松地将前面的示例扩展到多个文件from()
:
tasks.register<Copy>("copyReportsForArchiving") {
from(layout.buildDirectory.file("reports/my-report.pdf"), layout.projectDirectory.file("src/docs/manual.pdf"))
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyReportsForArchiving', Copy) {
from layout.buildDirectory.file("reports/my-report.pdf"), layout.projectDirectory.file("src/docs/manual.pdf")
into layout.buildDirectory.dir("toArchive")
}
现在有两个文件被复制到存档目录中。您还可以使用多个语句来执行相同的操作,如深度文件复制from()
部分的第一个示例所示。
现在考虑另一个例子:如果您想复制目录中的所有 PDF 而不必指定每个 PDF,该怎么办?为此,请将包含和/或排除模式附加到复制规范。这里我们使用字符串模式仅包含 PDF:
tasks.register<Copy>("copyPdfReportsForArchiving") {
from(layout.buildDirectory.dir("reports"))
include("*.pdf")
into(layout.buildDirectory.dir("toArchive"))
}
tasks.register('copyPdfReportsForArchiving', Copy) {
from layout.buildDirectory.dir("reports")
include "*.pdf"
into layout.buildDirectory.dir("toArchive")
}
需要注意的一件事是,如下图所示,仅reports
复制直接驻留在该目录中的 PDF:
您可以使用 Ant 风格的 glob 模式 ( **/*
) 将文件包含在子目录中,如本更新示例中所示: