管理任务

默认情况下,每个新创建的任务都是 org.gradle.api.DefaultTask 类型,即 org.gradle.api.Task 的标准实现。 DefaultTask 类中的所有字段都标记为私有。 这意味着只能通过其公共 getter 和 setter 方法来访问它们。 值得庆幸的是,Groovy 为您提供了一些语法糖,允许您按字段名称使用字段。 在幕后,Groovy 会为您调用该方法。 在本节中,我们将通过示例探讨任务最重要的特征。

管理项目版本

为了演示 DefaultTask 类的属性和方法,我将结合第 3 章中的 To Do 应用程序进行讲解。现在,您已经拥有了一般的构建基础架构,可以轻松添加功能。通常情况下,功能集被归类为版本。为了标识每个版本,我们会在交付产品中添加一个唯一的版本号。

许多企业或开源项目都有自己的版本控制策略。 回想一下您曾经参与过的一些项目。 通常,您分配特定的版本编号方案(例如,用点分隔的主要版本号和次要版本号,例如 1.2)。 您还可能会遇到附加 SNAPSHOT 指示符的项目版本,以指示构建的项目工件处于开发状态。 您已经在第 3 章中通过为项目属性 version 设置字符串值来为项目分配了版本。 使用字符串数据类型非常适合简单的用例,但是如果您想知道项目的确切次要版本怎么办? 您必须解析字符串值,搜索点字符,并过滤掉标识次要版本的子字符串。 用实际的类来表示版本不是更容易吗?

您可以轻松地使用类的字段来设置、检索和修改编号方案的特定部分。 你还可以走得更远。 通过将版本信息外部化到持久数据存储(例如文件或数据库),您将避免修改构建脚本本身来更改项目版本。 图 4.4 说明了构建脚本、保存版本信息的属性文件和数据表示类之间的交互。 您将在接下来的部分中创建并学习如何使用所有这些文件。

image 2024 03 08 17 34 25 336
Figure 1. 图 4.4 项目版本是在构建脚本运行时从属性文件中读取的。 ProjectVersion 数据类已实例化。 每个版本分类器都被转换为数据类的字段值。 ProjectVersion 的实例被分配给项目的版本属性。

您越想自动化项目生命周期,就必须能够以编程方式控制版本控制方案。 这是一个示例:您的代码已通过所有功能测试并准备好交付。 您项目的当前版本是 1.2-SNAPSHOT。 在构建最终的 WAR 文件之前,您需要将其设为发布版本 1.2 并自动将其部署到生产服务器。 其中每个步骤都可以通过创建任务来建模:一个任务用于修改项目版本,一个任务用于部署 WAR 文件。 让我们通过在项目中实施灵活的版本管理,将您对任务的了解提升到一个新的水平。

声明任务动作

操作(action)是任务中放置构建逻辑的适当位置。 Task 接口为您提供了两个相关的方法来声明任务(action)操作:doFirst(Closure) 和 doLast(Closure)。 当任务执行时,定义为闭包参数的动作逻辑会依次执行。

您将通过添加一个名为 printVersion 的单个任务来轻松开始。 该任务的目的是打印出当前的项目版本。 将此逻辑定义为该任务的最后一个操作,如以下代码片段所示:

version = '0.1-SNAPSHOT'

task printVersion {
    doLast {
        println "Version: $version"
    }
}

当使用 gradle printVersion 执行任务时,您应该看到正确的版本号:

$ gradle printVersion
:printVersion
Version: 0.1-SNAPSHOT

通过使用 doFirst 方法,可以获得与任务的第一个操作相同的结果:

task printVersion {
    doFirst {
        println "Version: $version"
    }
}

向现有任务添加操作

到目前为止,您只向任务 printVersion 添加了一个操作,作为第一个操作或最后一个操作。 但您并不限于每个任务执行一个操作。 事实上,即使在创建任务之后,您也可以根据需要添加任意数量的操作。 在内部,每个任务都保存一个任务操作列表。 在运行时,它们按顺序执行。 让我们看一下示例任务的修改版本:

// 任务的初始声明可以包含第一个和最后一个操作
task printVersion {
    doFirst {
        println "Before reading the project version"
    }
    doLast {
        println "Version: $version"
    }
}
// 附加 doFirst 闭包被插入到操作列表的开头
printVersion.doFirst { println "First action" }
// 使用 doLast 别名将闭包添加到操作列表末尾
printVersion.doLast { println "Last action" }

如清单所示,可以通过向现有任务添加操作来对其进行操作。 如果您想为不是您自己编写的任务执行自定义逻辑,这尤其有用。 例如,您可以将 doFirst 操作添加到 Java 插件的 compileJava 任务中,以检查项目是否至少包含一个 Java 源文件。

访问 DefaultTask 属性

接下来,我们将改进输出版本号的方式。Gradle 提供了一个基于日志库 SLF4J 的日志记录器。除了常规的日志级别(DEBUG、ERROR、INFO、TRACE、WARN),它还增加了一些额外的级别。日志记录器实例可通过任务的方法之一直接访问。现在,我们将使用日志级别 QUIET 打印版本号:

task printVersion << {
    logger.quiet "Version: $version"
}

看看访问任务属性之一是多么容易吗? 我想向您展示另外两个属性: group 和 description。 两者都是任务文档的一部分。description 属性表示任务目的的简短定义,而 group 则定义任务的逻辑分组。 创建任务时,您将设置这两个属性的值作为参数:

看到访问其中一个任务属性是多么容易了吗?我还想向你展示另外两个属性: group 和 description。这两个属性都是任务文档的一部分。描述属性代表任务目的的简短定义,而组定义了任务的逻辑分组。在创建任务时,你可以将这两个属性的值设置为参数:

task printVersion(group: 'versioning', description: 'Prints project version.') << {
    logger.quiet "Version: $version"
}

或者,您也可以通过调用 setter 方法来设置属性,如以下代码片段所示:

task printVersion {
    group = 'versioning'
    description = 'Prints project version.'

    doLast {
        logger.quiet "Version: $version"
    }
}

运行 gradle 任务时,您会看到该任务显示在正确的任务存储桶中,并且能够描述自身:

gradle tasks
:tasks
...
Versioning tasks
----------------
printVersion - Prints project version.
...

尽管设置任务的描述和分组是可选的,但为所有任务分配值始终是一个好主意。 它将使最终用户更容易识别任务的功能。 接下来,我们将回顾定义任务之间依赖关系的复杂性。

定义任务依赖

方法 dependsOn 允许声明对一个或多个任务的依赖关系。 您已经看到,Java 插件通过创建任务图来对完整任务生命周期(如构建任务)进行建模,从而广泛使用了这一概念。 以下清单显示了使用 dependsOn 方法应用任务依赖关系的不同方法。

清单 4.1 应用任务依赖关系
task first << { println "first" }
task second << { println "second" }

// 分配多个任务依赖关系
task printVersion(dependsOn: [second, first]) << {
    logger.quiet "Version: $version"
}

task third << { println "third" }
// 声明依赖项时按名称引用任务
third.dependsOn('printVersion')

您将通过从命令行调用第三个任务来执行任务依赖链:

$ gradle -q third
first
second
Version: 0.1-SNAPSHOT
third

如果您仔细查看任务执行顺序,您可能会对结果感到惊讶。 任务 printVersion 声明对第二个和第一个任务的依赖关系。 您难道没有预料到第二个任务会在第一个任务之前执行吗? 在 Gradle 中,任务执行顺序是不确定的。

任务依赖执行顺序

重要的是要了解 Gradle 不保证任务依赖项的执行顺序。 方法调用 dependsOn 仅定义依赖任务需要预先执行。 Gradle 的理念是声明在给定任务之前应该执行什么,而不是如何执行。 如果您使用的是像 Ant 那样强制定义其依赖项的构建工具,那么这个概念尤其难以理解。 在 Gradle 中,执行顺序由任务的输入/输出规范自动确定,您将在本章后面看到。 这种架构设计决策有很多好处。 一方面,您不需要了解整个任务依赖关系链即可进行更改,这提高了代码的可维护性并避免了潜在的破坏。 另一方面,由于您的构建不必严格按顺序执行,因此已启用并行任务执行,这可以显着缩短构建执行时间。

终结器任务

在实践中,您可能会发现自己遇到这样的情况:在执行依赖于某个资源的任务后需要清理该资源。 此类资源的典型用例是针对已部署的应用程序运行集成测试所需的 Web 容器。 Gradle 针对这种情况的解决方案是终结器任务,这些任务是计划运行的常规 Gradle 任务,即使终结任务失败也是如此。 以下代码片段演示了如何使用 Task 方法 FinalizedBy 来使用特定的终结器任务:

task first << { println "first" }
task second << { println "second" }
first.finalizedBy second // 声明一项任务已由另一项任务完成

确定一个任务的后续任务。

你会发现执行第一个任务会自动触发名为第二个的任务:

$ gradle -q first
first
second

第 7 章借助真实示例更深入地介绍了终结器任务的概念。 在下一节中,您将编写一个 Groovy 类以允许对版本控制方案进行更细粒度的控制。

添加任意代码

现在是时候回到我关于 Gradle 在构建脚本中定义通用 Groovy 代码的能力的声明了。 实际上,您可以按照在 Groovy 脚本或类中习惯的方式编写类和方法。 在本部分中,您将创建版本的类表示。 在 Java 中,遵循 bean 约定的类称为普通 Java 对象 (POJO)。 根据定义,它们通过 getter 和 setter 方法公开其字段。 随着时间的推移,手工编写这些方法会变得非常烦人。 POGO,Groovy 的 POJO 等价物,只需要您声明不带访问修饰符的属性。 它们的 getter 和 setter 方法本质上是在字节码生成时添加的,因此在运行时可用。 在下一个清单中,您分配 POGO ProjectVersion 的一个实例。 实际值在构造函数中设置。

清单 4.2 用 POGO 表示项目版本
// 由 java.lang.Object 表示的版本属性; Gradle 始终使用版本的 toString() 值
version = new ProjectVersion(0, 1)

class ProjectVersion {
    Integer major
    Integer minor
    Boolean release

    ProjectVersion(Integer major, Integer minor) {
        this.major = major
        this.minor = minor
        this.release = Boolean.FALSE
    }

    ProjectVersion(Integer major, Integer minor, Boolean release) {
        this(major, minor)
        this.release = release
    }

    @Override
    String toString() {
        // -SNAPSHOT 后缀仅在 release 属性为 false 时添加
        "$major.$minor${release ? '' : '-SNAPSHOT'}"
    }
}

运行修改后的构建脚本时,您应该看到任务 printVersion 生成与之前完全相同的结果。 不幸的是,您仍然需要手动编辑构建脚本来更改版本分类器。 接下来,您会将版本外部化到文件中并配置构建脚本来读取它。

了解任务配置

在开始编写代码之前,您需要在构建脚本旁边创建一个名为 version.properties 的属性文件。 对于每个版本类别(例如主要版本和次要版本),您将创建一个单独的属性。 以下键值对代表初始版本 0.1-SNAPSHOT:

major = 0
minor = 1
release = false

添加任务配置块

清单 4.3 声明了一个名为 loadVersion 的任务,用于从属性文件中读取版本分类器,并将新创建的 ProjectVersion 实例分配给项目的版本字段。 乍一看,该任务可能看起来与您之前定义的任何其他任务相似。 但如果仔细观察,您会发现您没有定义操作或使用左移运算符。 Gradle 将其称为任务配置。

清单 4.3 编写任务配置
// Project接口提供File方法; 它创建一个相对于项目目录的 java.io.File 实例。
ext.versionFile = file('version.properties')

// 任务配置是在没有左移运算符的情况下定义的。
task loadVersion { // 在配置阶段执行
    project.version = readVersion()
}

ProjectVersion readVersion() {
    logger.quiet 'Reading the version file.'

    if(!versionFile.exists()) {
        // 如果版本文件不存在,则抛出 GradleException 并附带相应的错误消息。
        throw new GradleException("Required version file does not exist: $versionFile.canonicalPath")
    }
    Properties versionProps = new Properties()
    // Groovy 的文件实现添加了使用新创建的 InputStream 读取文件的方法。
    versionFile.withInputStream { stream ->
        versionProps.load(stream)
    }
    // 在 Groovy 中,如果 return 关键字是方法中的最后一个语句,则可以省略它。
    new ProjectVersion(versionProps.major.toInteger(), versionProps.minor.toInteger(), versionProps.release.toBoolean())
}

如果您现在运行 printVersion,您将看到新任务 loadVersion 首先被执行。 尽管没有打印任务名称,但您知道这一点,因为构建输出会打印您添加到其中的日志记录语句:

$ gradle printVersion
Reading the version file.
:printVersion
Version: 0.1-SNAPSHOT

您可能会问自己为什么要调用该任务。 当然,您没有声明对它的依赖关系,也没有在命令行上调用该任务。 任务配置块始终在任务操作之前执行。 充分理解此行为的关键是 Gradle 构建生命周期。 让我们仔细看看每个构建阶段。

image 2024 03 08 18 23 35 596
Figure 2. 图 4.5 Gradle 构建生命周期中的构建阶段顺序

Gradle 的构建生命周期阶段

每当您执行 Gradle 构建时,都会运行三个不同的生命周期阶段:初始化、配置和执行。 图 4.5 直观地展示了构建阶段的运行顺序及其执行的代码。

在初始化阶段,Gradle 为您的项目创建一个 Project 实例。 您给定的构建脚本仅定义一个项目。 在多项目构建的背景下,此构建阶段变得更加重要。 根据您正在执行的项目,Gradle 会确定哪些项目依赖项需要参与构建。 请注意,在此构建阶段不会执行当前现有的构建脚本代码。 当您将待办事项应用程序模块化为多项目构建时,这将在第 6 章中发生变化。

接下来的构建阶段是配置阶段。 在内部,Gradle 构建了将参与构建的任务的模型表示。 增量构建功能确定是否需要运行模型中的任何任务。 此阶段非常适合设置项目或特定任务所需的配置。

请记住,任何配置代码都会在项目的每次构建中执行 - 即使您只是执行 gradle 任务。

在执行阶段,任务按照正确的顺序执行。 执行顺序由它们的依赖关系决定。 被视为最新的任务将被跳过。 例如,如果任务 B 依赖于任务 A,那么当您在命令行上运行 gradle B 时,执行顺序将为 A → B。

正如您所看到的,Gradle 的增量构建功能与生命周期紧密集成。 在第 3 章中,您看到 Java 插件大量使用了此功能。 仅当任何 Java 源文件与上次运行构建时不同时,compileJava 任务才会运行。 最终,此功能可以显着提高构建的性能。 在下一节中,我将展示如何使用增量构建功能来完成您自己的任务。

声明任务输入和输出

Gradle 通过比较两个构建之间任务的输入和输出的快照来确定任务是否是最新的,如图 4.6 所示。 如果自上次任务执行以来输入和输出没有更改,则任务被认为是最新的。 因此,任务仅在输入和输出不同时运行; 否则,它会被跳过。

image 2024 03 08 18 44 04 730
Figure 3. 图 4.6 Gradle 确定任务是否需要通过其输入/输出执行。

输入可以是一个目录、一个或多个文件或任意属性。 任务的输出是通过目录或 1…​n 个文件定义的。 输入和输出被定义为类 DefaultTask 中的字段,并具有直接的类表示,如图 4.7 所示。

image 2024 03 08 18 45 24 540
Figure 4. 图 4.7 类 DefaultTask 定义任务输入和输出。

让我们看看这个功能的实际效果。 想象一下,您想要创建一个任务来为生产版本准备项目的可交付成果。 为此,您需要将项目版本从 SNAPSHOT 更改为发布版本。 以下清单定义了一个新任务,它将布尔值 true 分配给版本属性发布。 该任务还将版本更改传播到属性文件。

清单 4.4 将项目版本切换到生产就绪状态
task makeReleaseVersion(group: 'versioning', description: 'Makes project a release version.') << {
    version.release = true
    // Ant 任务 propertyfile 提供了一种修改属性文件的便捷方法。
    ant.propertyfile(file: versionFile) {
        entry(key: 'release', type: 'string', operation: '=', value: 'true')
    }
}

正如预期的那样,运行任务将更改版本属性并将新值保留到属性文件中。 以下输出演示了该行为:

$ gradle makeReleaseVersion
:makeReleaseVersion

$ gradle printVersion
:printVersion
Version: 0.1

任务 makeReleaseVersion 可能是将 WAR 文件部署到生产服务器的另一个生命周期任务的一部分。 您可能痛苦地意识到部署可能会出错。 网络可能出现故障,导致无法访问服务器。 修复网络问题后,您将需要再次运行部署任务。 由于任务 makeReleaseVersion 被声明为部署任务的依赖项,因此它会自动重新运行。 等等,您已经将项目版本标记为生产就绪,对吧? 不幸的是,Gradle 任务不知道这一点。 为了让它意识到这一点,您将声明它的输入和输出,如下一个清单所示。

清单 4.5 通过输入/输出添加增量构建支持
task makeReleaseVersion(group: 'versioning', description: 'Makes project a release version.') { // 输入/输出在配置阶段声明
    // 将版本发布属性声明为输入
    inputs.property('release', version.release)
    // 由于版本文件将被修改,因此它被声明为输出文件属性
    outputs.file versionFile

    doLast {
        version.release = true
        ant.propertyfile(file: versionFile) {
            entry(key: 'release', type: 'string', operation: '=', value: 'true')
        }
    }
}

您将想要执行的代码移至 doLast 操作闭包中,并从任务声明中删除了左移运算符。 完成此操作后,您现在可以清楚地分离配置和操作代码。

任务输入/输出计算

请记住,任务输入和输出是在配置阶段计算的,以连接任务依赖性。 这就是为什么它们需要在配置块中定义。 为了避免意外行为,请确保分配给输入和输出的值在配置时可访问。 如果您需要实现编程输出计算,TaskOutputs 上的 upToDateWhen(Closure) 方法会派上用场。 与常规输入/输出相反,此方法是在执行时计算的。 如果闭包返回 true,则该任务被认为是最新的。

现在,如果您执行任务两次,您将看到 Gradle 已经知道项目版本已设置为发布并自动跳过任务执行:

$ gradle makeReleaseVersion
:makeReleaseVersion

$ gradle makeReleaseVersion
:makeReleaseVersion UP-TO-DATE

如果您不在属性文件中手动更改发布属性,则任务 makeReleaseVersion 的任何后续运行都将被标记为最新。

到目前为止,您已经使用 Gradle 的 DSL 在构建脚本中创建和修改任务。 每个任务都由在 Gradle 配置阶段为您实例化的实际任务对象提供支持。 在许多情况下,简单的任务就可以完成工作。 但是,有时您可能希望完全控制任务实施。 在下一节中,您将以自定义任务实现的形式重写任务 makeReleaseVersion。

编写和使用自定义任务

任务 makeReleaseVersion 中的操作逻辑相当简单。 代码可维护性目前显然不是问题。 然而,在处理项目时,您会注意到,随着您需要添加的逻辑越多,简单的任务的规模就会迅速增大。 将出现将代码结构化为类和方法的需要。 您应该能够应用与常规生产源代码中所习惯的相同的编码实践,对吧? Gradle 没有建议编写任务的特定方式。 您可以完全控制构建源代码。 您选择的编程语言(无论是 Java、Groovy 还是任何其他基于 JVM 的语言)以及任务的位置都由您决定。

自定义任务由两个组件组成:封装逻辑行为的自定义任务类(也称为任务类型)和为任务类公开的属性提供值以配置行为的实际任务。 Gradle 将这些任务称为增强型任务。

可维护性只是编写自定义任务类的优点之一。 因为您正在处理一个实际的类,所以任何方法都可以通过单元测试进行完全测试。 测试构建代码超出了本章的范围。 如果您想了解更多信息,请随时跳到第 7 章。增强任务相对于简单任务的另一个优势是可重用性。 自定义任务公开的属性可以从构建脚本中单独设置。 考虑到增强任务的好处,我们来讨论编写自定义任务类。

编写自定义任务类

正如本章前面提到的,Gradle 为构建脚本中的每个简单任务创建一个 DefaultTask 类型的实例。 创建自定义任务时,您只需创建一个扩展 DefaultTask 的类。 以下清单演示了如何将 makeReleaseVersion 的逻辑表达为用 Groovy 编写的自定义任务类 ReleaseVersionTask。

清单4.6 自定义任务实现
// 编写扩展 Gradle 默认任务实现的自定义任务
class ReleaseVersionTask extends DefaultTask {
    // 通过注释声明自定义任务的输入/输出
    @Input Boolean release
    @OutputFile File destFile

    // 在构造函数中设置任务的组和描述属性
    ReleaseVersionTask() {
        group = 'versioning'
        description = 'Makes project a release version.'
    }

    @TaskAction
    void start() { // 注解声明要执行的方法
        project.version.release = true
        ant.propertyfile(file: destFile) {
            entry(key: 'release', type: 'string', operation: '=', value: 'true')
        }
    }
}

在清单中,您没有使用 DefaultTask 的属性来声明其输入和输出。 相反,您可以使用 org.gradle.api.tasks 包中的注释。

通过注释表达输入和输出

任务输入和输出注释为您的实现添加了语义糖。 它们不仅具有与 TaskInputs 和 TaskOutputs 的方法调用相同的效果,而且还充当自动文档。 乍一看,您确切地知道什么数据将作为输入以及任务会产生什么输出工件。 在探索这个包的 Javadoc 时,您会发现 Gradle 为您提供了广泛的注释。

在自定义任务类中,您使用 @Input 注释来声明输入属性释放,并使用注释 @OutputFile 来定义输出文件。 将输入和输出注释应用于字段并不是唯一的选择。 您还可以注释字段的 getter 方法。

任务输入验证

注释 @Input 将在配置时验证属性的值。 如果值为 null,Gradle 将抛出 TaskValidationException。 要允许空值,请使用 @Optional 注释标记该字段。

使用自定义任务

您通过创建操作方法实现了自定义任务类,并通过字段公开其可配置属性。 但实际如何使用它呢? 在构建脚本中,您需要创建一个 ReleaseVersionTask 类型的任务,并通过为其属性分配值来设置输入和输出,如下列表所示。 将其视为创建特定类的新实例并在构造函数中设置其字段的值。

清单 4.7 ReleaseVersionTask 类型的任务
// 定义 ReleaseVersionTask 类型的增强任务
task makeReleaseVersion(type: ReleaseVersionTask) {
    // 设置自定义任务属性
    release = version.release
    destFile = versionFile
}

正如预期的那样,如果运行增强任务 makeReleaseVersion,其行为方式将与简单任务完全相同。 与简单任务实现相比,您拥有的一大优势是您公开了可以单独分配的属性。

应用的自定义任务可重用性

假设您想在另一个项目中使用自定义任务。 在该项目中,要求是不同的。 POGO 版本公开了不同的字段来表示版本控制方案,如下列表所示。

清单 4.8 不同版本 POGO 实现
class ProjectVersion {
    Integer min
    Integer maj
    Boolean prodReady

    @Override
    String toString() {
        "$maj.$min${prodReady? '' : '-SNAPSHOT'}"
    }
}

此外,项目所有者决定将版本文件命名为 project-version.properties,而不是 version.properties。 增强型任务如何适应这些要求? 您只需为公开的属性分配不同的值,如以下列表所示。 自定义任务类可以灵活处理不断变化的需求。

清单 4.9 设置任务 makeReleaseVersion 的各个属性值
task makeReleaseVersion(type: ReleaseVersionTask) {
    // POGO版本表示使用字段prodReady来指示发布标志
    release = version.prodReady
    // 分配不同版本的文件对象
    destFile = file('project-version.properties')
}

Gradle 为常用功能提供了各种开箱即用的自定义任务,例如复制和删除文件或创建 ZIP 存档。 在下一节中,我们将仔细研究其中的一些。

Gradle 的内置任务类型

你还记得上一次人工生产部署出错的情景吗?我敢打赌,你的脑海中一定还浮现着一幅生动的画面:愤怒的客户给你的支持团队打电话,老板敲你的门询问出了什么问题,而你的同事则疯狂地试图找出启动应用程序时抛出堆栈跟踪的根本原因。在手动发布流程中,忘记一个步骤都可能是致命的。

image 2024 03 08 19 24 08 409
Figure 5. 图4.8 发布项目的任务依赖

让我们成为专业人士,并为构建生命周期各个方面的自动化而感到自豪。 能够以自动方式修改项目的版本控制方案只是对发布过程进行建模的第一步。 为了能够从失败的部署中快速恢复,良好的回滚策略至关重要。 事实证明,备份最新的稳定应用程序可交付成果以进行重新部署是非常有价值的。 您将使用 Gradle 附带的一些任务类型来为您的待办事项应用程序实现此过程的部分内容。

这就是你要做的事情。 在将任何代码部署到生产环境之前,您需要创建一个发行版。 它将作为未来失败部署的后备交付成果。 发行版是一个 ZIP 文件,包含 Web 应用程序存档、所有源文件和版本属性文件。 创建发行版后,文件将被复制到备份服务器。 备份服务器可以通过已安装的共享驱动器进行访问,也可以通过 FTP 传输文件。 因为我不想让这个示例太复杂而难以理解,所以您只需将其复制到子目录 build/backup 中即可。 图 4.8 说明了您希望执行任务的顺序。

使用任务类型

Gradle 的内置任务类型是 DefaultTask 的派生类。 因此,它们可以在构建脚本中的增强任务中使用。 Gradle 提供了广泛的任务类型,但出于本示例的目的,您将仅使用其中两种。 以下列表显示了发布软件生产版本时的任务类型 Zip 和 Copy。 您可以在 DSL 指南中找到完整的任务参考。

清单 4.10 使用任务类型来备份压缩版本发行版
task createDistribution(type: Zip, dependsOn: makeReleaseVersion) {
    // 隐式引用 War 任务的输出
    from war.outputs.files

    // 获取所有源文件并将它们放入 ZIP 文件的 src 目录中
    from(sourceSets*.allSource) {
        into 'src'
    }
    from(rootDir) {
        // 将版本文件添加到 ZIP
        include versionFile.name
    }
}

task backupReleaseDistribution(type: Copy) {
    // 对 createDistribution 输出的隐式引用
    from createDistribution.outputs.files
    into "$buildDir/backup"
}

task release(dependsOn: backupReleaseDistribution) << {
    logger.quiet 'Releasing the project...'
}

在此列表中,有不同的方法告诉 Zip 和 Copy 任务要包含哪些文件以及将它们放在哪里。 这里使用的许多方法都来自超类 AbstractCopyTask,如图4.9所示。 有关可用选项的完整列表,请参阅类的 Javadoc。

您使用的任务类型提供的配置选项比示例中显示的配置选项多得多。 同样,有关可用选项的完整列表,请参阅 DSL 参考或 Javadocs。 接下来,我们将更深入地了解它们的任务依赖性。

任务依赖性推断

您可能已经在清单中注意到,两个任务之间的任务依赖关系是通过 dependentOn 方法显式声明的。 但是,某些任务不会建立对其他任务的直接依赖关系(例如,war 的 createDistribution)。 Gradle 如何知道预先执行依赖任务? 通过使用一个任务的输出作为另一任务的输入,可以推断出依赖性。 因此,相关任务会自动运行。 让我们看看完整的任务执行图:

$ gradle release
:makeReleaseVersion
:compileJava
:processResources UP-TO-DATE
:classes
:war
:createDistribution
:backupReleaseDistribution
:release
Releasing the project...
image 2024 03 08 19 37 37 454
Figure 6. 图 4.9 任务类型 Zip 和 Copy 的继承层次结构

运行构建后,您应该在 build/distributions 目录中找到生成的 ZIP 文件,该目录是存档任务的默认输出目录。 您可以通过设置属性 destinationDir 轻松分配不同的分发输出目录。 以下目录树显示了构建生成的相关工件:

.
├── build
│   ├── backup
│   │   └── todo-webapp-0.1.zip
│   ├── distributions
│   │   └── todo-webapp-0.1.zip
│   └── libs
│       └── todo-webapp-0.1.war
├── buildxxx.gradle
├── src
└── version.properties

任务类型内置了增量构建支持。如果您不更改任何源文件,连续多次运行任务会将它们标记为最新。 接下来,您将学习如何定义一个任务,该任务的行为取决于灵活的任务名称。

任务规则

有时您可能会发现自己编写了多个执行类似操作的任务。 例如,假设您想通过另外两项任务来扩展版本管理功能:一项是增加项目的主要版本,另一项是为次要版本分类器执行相同的工作。 这两个任务还应该保留对版本文件的更改。 如果您比较以下清单中两个任务的 doLast 操作,您可以发现您基本上重复了代码并对它们进行了微小的更改。

清单 4.11 声明递增版本分类器的任务
task incrementMajorVersion(group: 'versioning', description: 'Increments project major version.') << {
    String currentVersion = version.toString()
    ++version.major
    String newVersion = version.toString()
    logger.info "Incrementing major project version: $currentVersion -> $newVersion"

    // 使用 Ant 任务 propertyfile 来增加属性文件中的特定属性
    ant.propertyfile(file: versionFile) {
        entry(key: 'major', type: 'int', operation: '+', value: 1)
    }
}

task incrementMinorVersion(group: 'versioning', description: 'Increments project minor version.') << {
    String currentVersion = version.toString()
    ++version.minor
    String newVersion = version.toString()
    logger.info "Incrementing minor project version: $currentVersion -> $newVersion"

    // 使用 Ant 任务 propertyfile 来增加属性文件中的特定属性
    ant.propertyfile(file: versionFile) {
        entry(key: 'minor', type: 'int', operation: '+', value: 1)
    }
}

如果您在版本为 0.1-SNAPSHOT 的项目上运行 gradleincrementMajorVersion,您将看到版本已提升至 1.1-SNAPSHOT。 在 INFO 日志级别运行可以查看更详细的输出信息:

$ gradle incrementMajorVersion –i
:incrementMajorVersion
Incrementing major project version: 0.1-SNAPSHOT -> 1.1-SNAPSHOT
[ant:propertyfile] Updating property file: /Users/benjamin/books/
➥ gradle-in-action/code/chapter4/task-rule/version.properties

有两个单独的任务就可以了,但是您当然可以改进此实现。 最后,您对维护重复的代码不感兴趣。

任务规则命名模式

Gradle 还引入了任务规则的概念,根据任务名称模式执行特定逻辑。该模式由两部分组成:任务名称的静态部分和占位符。它们共同构成了动态任务名称。如果要在前面的示例中应用任务规则,命名模式将如下所示:increment<Classifier>Version。在命令行上执行任务规则时,可以用驼峰字母符号指定分类器占位符(例如,incrementMajorVersion 或 incrementMinorVersion)。

实践中的任务规则

Gradle 的一些核心插件很好地利用了任务规则。 Java 插件定义的任务规则之一是 clean<TaskName>,它删除指定任务的输出。 例如,从命令行运行 gradle cleanCompileJava 会删除所有生产代码类文件。

声明任务规则

您刚刚阅读了关于定义任务规则命名模式的内容,但如何在构建脚本中实际声明任务规则呢?要在项目中添加任务规则,首先需要获取 TaskContainer 的引用。获得引用后,就可以调用 addRule(String, Closure) 方法。第一个参数提供描述(例如任务名称模式),第二个参数声明要执行的闭包,以应用该规则。遗憾的是,无法像简单任务那样通过 Project 方法直接创建任务规则,如图 4.10 所示。

image 2024 03 12 16 30 00 000
Figure 7. 图4.10 可以通过调用项目实例的方法直接添加简单任务。 任务规则只能通过任务容器添加,因此您需要首先通过调用 getTasks() 方法来获取对它的引用。

在基本了解如何在项目中添加任务规则后,就可以开始编写实际的闭包实现了。下一个列表演示了应用任务规则如何成为一种极具表现力的工具,以实现具有类似逻辑的任务操作。

// 添加任务规则并提供说明
tasks.addRule("Pattern: increment<Classifier>Version – Increments the project version classifier.") { String taskName ->
    if(taskName.startsWith('increment') && taskName.endsWith('Version')) { // 检查预定义模式的任务名称
        task(taskName) << { // 使用 doLast 操作动态添加以提供的模式命名的任务
            String classifier = (taskName - 'increment' - 'Version') .toLowerCase() // 从完整任务名称中提取类型字符串

            String currentVersion = version.toString()

            switch(classifier) {
                case 'major': ++version.major
                    break
                case 'minor': ++version.minor
                    break
                default: throw new GradleException("Invalid version type '$classifier. Allowed types: ['Major', 'Minor']")
            }
            String newVersion = version.toString()
            logger.info "Incrementing $classifier project version: $currentVersion -> $newVersion"
            ant.propertyfile(file: versionFile) {
                entry(key: classifier, type: 'int', operation: '+', value: 1)
            }
        }
    }
}

在项目中添加任务规则后,你会发现在运行帮助任务 tasks 时,它被列在一个名为规则(Rules)的特定任务组下:

$ gradle tasks
...
Rules
-----
Pattern: increment<Classifier>Version - Increments project version type

任务规则不能像其他简单任务或增强任务那样单独分组。任务规则即使是由插件声明的,也总是显示在该组下。

在 buildSrc 目录中构建代码

您已经看到构建脚本代码的增长速度有多快。 在本章中,您已经在构建脚本中创建了两个 Groovy 类:ProjectVersion 和自定义任务 ReleaseVersionTask。 这些类是与项目一起移动到 buildSrc 目录的完美候选者。 buildSrc 目录是放置构建代码的替代位置,也是良好软件开发实践的真正推动者。 您将能够按照在任何其他项目中习惯的方式构建代码,甚至为其编写测试。

Gradle 标准化了 buildSrc 目录下源文件的布局。 Java 代码需要位于目录 src/main/java 中,而 Groovy 代码预计位于目录 src/main/groovy 中。 在这些目录中找到的任何代码都会自动编译并放入常规 Gradle 构建脚本的类路径中。 buildSrc 目录是组织代码的好方法。 因为您正在处理类,所以您也可以将它们放入特定的包中。 您将使它们成为 com.manning.gia 包的一部分。 以下目录结构显示了新位置中的 Groovy 类:

.
├── buildxxx.gradle
├── buildSrc
│   └── src
│       └── main
│           └── groovy
│               └── com
│                   └── manning
│                       └── gia
│                           ├── ProjectVersion.groovy
│                           └── ReleaseVersionTask.groovy
├── src
│   └── ...
└── version.properties

请注意,将类提取到自己的源文件中需要一些额外的工作。在构建脚本中定义类与在单独源文件中定义类的区别在于,你需要从 Gradle API 中导入类。下面的代码片段显示了自定义任务 ReleaseVersionTask 的包和导入声明:

package com.manning.gia

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

class ReleaseVersionTask extends DefaultTask {
    (...)
}

反过来,您的构建脚本需要从 buildSrc 中导入已编译的类(例如 com.manning.gia.ReleaseVersionTask)。下面的控制台输出显示了在命令行上调用的任务之前运行的编译任务:

$ gradle makeReleaseVersion
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes
:buildSrc:jar
:buildSrc:assemble
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test
:buildSrc:check
:buildSrc:build
:makeReleaseVersion UP-TO-DATE

buildSrc 目录被视为其自己的 Gradle 项目,由路径 :buildSrc 指示。 由于您没有编写任何单元测试,因此会跳过测试的编译和执行任务。 第 7 章完全致力于为 buildSrc 中的类编写测试。

在前面的部分中,您了解了使用 Gradle API 提供的简单任务、自定义任务类和特定任务类型的详细信息。 我们研究了任务操作和配置代码之间的差异,以及它们适当的用例。 您学到的一个重要教训是,操作和配置代码是在构建生命周期的不同阶段执行的。 本章的其余部分将讨论如何编写在触发特定生命周期事件时执行的代码。