挂钩到构建生命周期
作为构建脚本开发人员,您不仅限于编写任务操作或配置逻辑,这些操作或配置逻辑在不同的构建阶段进行计算。 有时,您需要在发生特定生命周期事件时执行代码。 生命周期事件可以在特定构建阶段之前、期间或之后发生。 执行阶段之后发生的生命周期事件的一个示例是构建的完成(Build finished)。
假设您希望在开发周期中尽早获得有关失败构建的反馈。 对失败构建的典型反应可能是您向团队中的所有开发人员发送电子邮件以恢复代码的完整性。 有两种方法可以编写回调来构建生命周期事件:在闭包内,或者使用 Gradle API 提供的侦听器接口的实现。 Gradle 不会引导您选择监听生命周期事件的选项之一。 这个选择由你。 使用侦听器实现的一大优势是,您正在处理一个可以通过编写单元测试来完全测试的类。 为了让您了解一些有用的生命周期挂钩,请参见图 4.11。

本书无法详细列出所有可用的生命周期钩子。许多生命周期回调方法都定义在 Project 和 Gradle 的接口中。Gradle 的 Javadocs 是一个很好的起点,可以为你的用例找到合适的事件回调。
不要害怕使用生命周期钩子。它们并不是 Gradle API 的秘密后门。相反,它们是有意提供的,因为 Gradle 无法预测企业构建的需求。 |
在下面两节中,我将演示如何在任务执行图填充完成后立即接收通知。为了充分了解任务执行图构建过程中发生了什么,我们首先来看看 Gradle 的内部工作原理。
内部任务图表示
在配置时,Gradle 确定执行阶段需要运行的任务的顺序。 如第 1 章所述,表示这些任务依赖关系的内部结构被建模为有向无环图 (DAG)。 图中的每个任务称为一个节点,每个节点通过有向边连接。 您很可能通过声明任务的 dependentOn 关系或利用隐式任务依赖干扰机制在节点之间创建了这些连接。 值得注意的是,DAG 从来不包含循环。 换句话说,以前执行过的任务将永远不会再次执行。 图 4.12 演示了之前建模的发布流程的 DAG 表示。


现在您对 Gradle 的内部任务图表示有了更好的了解,您将在构建脚本中编写一些代码来对其做出反应。
连接到任务执行图
回想一下您实现的 makeReleaseVersion 任务,该任务作为任务发布的依赖项自动执行。 您还可以通过编写生命周期挂钩来实现相同的目标,而不是编写任务来更改项目的版本以指示生产就绪。 由于构建在执行之前确切地知道哪些任务将参与构建,因此您可以查询任务图以检查其是否存在。 图4.13显示了访问任务执行图的相关接口及其方法。
接下来,您将放置生命周期挂钩。 清单 4.13 通过调用 whenReady 方法来注册一个在填充任务图后立即执行的闭包来扩展构建脚本。 因为您知道该逻辑在执行图中的任何任务之前运行,所以您可以完全删除任务 makeReleaseVersion 并省略 createDistribution 中的 dependentOn 声明。
// 注册填充任务图时调用的生命周期钩子
gradle.taskGraph.whenReady { TaskExecutionGraph taskGraph ->
// 检查任务执行图是否包含任务释放
if(taskGraph.hasTask(release)) {
if(!version.release) {
version.release = true
ant.propertyfile(file: versionFile) {
entry(key: 'release', type: 'string', operation: '=', value: 'true')
}
}
}
}
或者,您可以将此逻辑实现为侦听器,接下来您将执行此操作。
实现任务执行图监听器
通过侦听器连接到构建生命周期只需两个简单的步骤即可完成。 首先,通过在构建脚本中编写一个类来实现特定的侦听器接口。 其次,您在构建中注册侦听器实现。
监听任务执行图事件的接口由接口 TaskExecutionGraphListener 提供。 在撰写本文时,您只需要实现一种方法:graphPopulate(TaskExecutionGraph)。 图 4.14 显示了名为 ReleaseVersionListener 的监听器实现。
请记住,如果将侦听器添加到构建脚本中,您将无法直接访问 Project 实例。 相反,您可以充分利用 Gradle 的 API。 以下清单显示了如何通过调用发布任务上的 getProject() 方法来访问项目。

class ReleaseVersionListener implements TaskExecutionGraphListener {
final static String releaseTaskPath = ':release'
@Override
void graphPopulated(TaskExecutionGraph taskGraph) {
// 判断释放任务是否包含在执行图中
if(taskGraph.hasTask(releaseTaskPath)) {
List<Task> allTasks = taskGraph.allTasks
// 从执行图中所有任务的列表中过滤释放任务
Task releaseTask = allTasks.find {it.path == releaseTaskPath }
// 每个任务都知道它属于哪个项目
Project project = releaseTask.project
if(!project.version.release) {
project.version.release = true
project.ant.propertyfile(file: project.versionFile) {
entry(key: 'release', type: 'string', operation: '=',
value: 'true')
}
}
}
}
}
def releaseVersionListener = new ReleaseVersionListener()
// 将侦听器注册到任务执行图
gradle.taskGraph.addTaskExecutionGraphListener(releaseVersionListener)
您不仅限于在构建脚本中注册生命周期侦听器。 即使在执行任何项目任务之前,也可以应用生命周期逻辑来侦听 Gradle 事件。 在下一节中,我们将探索通过初始化脚本挂钩生命周期以自定义构建环境的选项。
初始化构建环境
假设您希望收到有关构建结果的通知。 每当构建完成时,您都会想知道它是成功还是失败。 您还希望能够确定已执行了多少任务。 Gradle 的核心插件之一是构建公告插件,它提供了一种将公告发送到本地通知系统(如 Snarl (Windows) 或 Growl (Mac OS X))的方法。 该插件会根据您的操作系统自动选择正确的通知系统。 图 4.15 显示了 Growl 呈现的通知。
您可以将该插件单独应用到每个项目,但为什么不使用 Gradle 提供的强大机制呢? 初始化脚本在评估和执行任何构建脚本逻辑之前运行。 您将编写一个初始化脚本,将该插件应用于您的任何项目,而无需手动干预。 在 <USER_HOME>/.gradle/init.d 下创建初始化脚本,如下目录树所示:
.
└── .gradle
└── init.d
└── build-announcements.gradle
只要文件扩展名与 .gradle
匹配,Gradle 就会执行在 init.d 下找到的每个初始化脚本。 因为您希望在执行任何其他构建脚本代码之前应用该插件,所以您将选择最适合处理这种情况的生命周期回调方法:Gradle#projectLoaded(Closure)。 以下代码片段显示了如何将构建公告插件应用到构建的根项目:
// 创建构建项目后执行关闭
gradle.projectsLoaded { Gradle gradle ->
gradle.rootProject {
apply plugin: 'build-announcements'
}
}
在这种情况下要学习的重要一课是,有些生命周期事件只有在适当的位置声明后才会被触发。例如,如果在 build.gradle 中声明了生命周期钩子 Gradle#projectsLoaded(Closure) 的闭包,它就不会被触发,因为项目创建发生在初始化阶段。
总结
每个 Gradle 构建脚本都包含两个基本构建块:一个或多个项目和任务。 这两个元素都深深植根于 Gradle 的 API 中,并且具有直接的类表示。 在运行时,Gradle 根据构建定义创建一个模型,将其存储在内存中,并让您可以通过方法访问它。 您了解到属性是控制构建行为的一种方法。 项目公开了开箱即用的标准属性。 此外,您可以在许多 Gradle 的域模型对象上定义额外的属性(例如,在项目和任务级别)来声明任意用户数据。
在本章后面,您了解了任务的细节。 例如,您实现了构建逻辑来控制存储在外部属性文件中的项目版本编号方案。 您首先向构建脚本添加简单的任务。 构建逻辑可以直接在任务的操作闭包中定义。 每个任务都派生自 org.gradle.api.DefaultTask 类。 因此,它加载了可通过其超类的方法访问的功能。
了解构建生命周期及其阶段的执行顺序对于初学者来说至关重要。 Gradle 明确区分了任务操作和任务配置。 通过闭包 doFirst 和 doLast 或其快捷符号 << 定义的任务操作在执行阶段运行。 在任务操作之外定义的任何其他代码都被视为配置,因此在配置阶段提前执行。
接下来,我们将注意力转向实现非功能性需求:构建执行性能、代码可维护性和可重用性。 您通过声明其输入和输出数据,向现有任务实现之一添加了增量构建支持。 如果数据在初始构建任务和后续构建任务之间没有变化,则跳过执行。 实施增量构建支持既简单又便宜。 如果做得正确,它可以显着提高构建的执行时间。 复杂的构建逻辑最好在自定义任务类中构建,这为您提供了面向对象编程的所有好处。 您通过将现有逻辑转移到 DefaultTask 的实现中来练习编写自定义任务类。 您还通过将可编译代码移动到 buildSrc 目录下来清理构建脚本。 Gradle 附带了一系列可重用的任务类型,例如 Zip 和 Copy。 您通过对发布项目的任务依赖关系链进行建模来合并这两种类型。
对 Gradle 内部结构的访问不仅限于模型。 您可以注册构建生命周期挂钩,以便在触发目标事件时执行代码。 例如,您编写了一个任务执行图生命周期挂钩作为闭包和侦听器实现。 初始化脚本可用于在所有构建中应用通用代码,例如生命周期侦听器。
您已经初步了解了使您能够声明对外部库的依赖关系的机制。 在下一章中,我们将通过详细讨论如何使用依赖项以及依赖项解析在幕后如何工作来加深您的知识。