第五十七章. 编写自定义任务类

Chapter 57. Writing Custom Task Classes

Gradle 支持两种类型的任务。一种是简单任务,你可以使用一个动作闭包来定义它。我们在《第六章,构建脚本基础》中已经看到过。对于这种类型的任务,动作闭包确定了任务的行为。这种类型的任务便于实现构建脚本中只执行一次的任务。
Gradle supports two types of task. One such type is the simple task, where you define the task with an action closure. We have seen these in Chapter 6, Build Script Basics. For this type of task, the action closure determines the behaviour of the task. This type of task is good for implementing one-off tasks in your build script.

另一种是增强任务,它把行为建立在任务中,并且任务提供了一些属性用于配置行为。我们在《第十五章,有关任务的详细信息》中看到过。大多数 Gradle 插件使用增强任务。通过增强任务,你不需要像使用简单任务一样实现任务行为。你只需要简单地声明该任务,并使用其属性来配置它。通过这种方式,增强任务允许让你在许多不同位置,甚至是跨不同构建,来复用某个行为。
The other type of task is the enhanced task, where the behaviour is built into the task, and the task provides some properties which you can use to configure the behaviour. We have seen these in Chapter 15, More about Tasks. Most Gradle plugins use enhanced tasks. With enhanced tasks, you don't need to implement the task behaviour as you do with simple tasks. You simply declare the task and configure the task using its properties. In this way, enhanced tasks let you reuse a piece of behaviour in many different places, possibly across different builds.

增强任务的行为和属性由任务的类定义。当你声明一个增强任务时,要指定该任务的类型或类。
The behaviour and properties of an enhanced task is defined by the task's class. When you declare an enhanced task, you specify the type, or class of the task.

在 Gradle 中实现自己的自定义任务类并不难。你可以使用你喜欢的任何语言来实现一个自定义任务类,只要它最终会被编译成字节码。在我们的示例中,我们将使用 Groovy 作为实现语言,但你也可以使用 Java 或 Scala。通常来说,使用 Groovy 是一种最简单的选择,因为 Gradle API 就是被设计为方便使用 Groovy 的。
Implementing your own custom task class in Gradle is easy. You can implement a custom task class in pretty much any language you like, provided it ends up compiled to bytecode. In our examples, we are going to use Groovy as the implementation language, but you could use, for example, Java or Scala. In general, using Groovy is the easiest option, because the Gradle API is designed to work well with Groovy.

57.1. 封装一个任务类

57.1. Packaging a task class

有几个地方可以放任务类的源码。
There are several places where you can put the source for the task class.

构建脚本
Build script

你可以直接在构建脚本中包含任务类。这样做的好处是,你不需要再另外做什么,这个任务类就会自动编译并包含在构建脚本的类路径中。但是在构建脚本之外,这个任务类是不可见的,因此你无法在定义这个它的构建脚本以外的地方复用这个任务类。
You can include the task class directly in the build script. This has the benefit that the task class is automatically compiled and included in the classpath of the build script without you having to do anything. However, the task class is not visible outside the build script, and so you cannot reuse the task class outside the build script it is defined in.

buildSrc 项目
buildSrc project

您可以将任务类的源码放在 rootProjectDir/buildSrc/src/main/rootvy 目录中。Gradle 将会编译和测试这个任务类,并使其在构建脚本的类路径中可用。这个任务类对该构建所使用的每个构建脚本都可见。然而,它在构建外部不可见,因此你无法在定义这个任务类的构建之外的其他地方来复用它。使用 buildSrc 项目方法能够保持任务声明——即任务应该做什么,与任务实现——即任务是怎么做的之间相互独立。
You can put the source for the task class in the rootProjectDir/buildSrc/src/main/groovy directory. Gradle will take care of compiling and testing the task class and making it available on the classpath of the build script. The task class is visible to every build script used by the build. However, it is not visible outside the build, and so you cannot reuse the task class outside the build it is defined in. Using the buildSrc project approach keeps separate the task declaration - that is, what the task should do - from the task implementation - that is, how the task does it.

有关buildSrc 项目的更详细信息,请参阅《第五十九章,组织构建逻辑》。
See Chapter 59, Organizing Build Logic for more details about the buildSrc project.

独立项目
Standalone project

你可以为你的任务类创建单独的项目。这个项目会生成和发布一个 JAR,你可以在多个构建中使用并分享出去。通常,这个 JAR 可能包含一些定制插件,或者将多个相关任务类捆绑到一个库中,或两者皆有。
You can create a separate project for your task class. This project produces and publishes a JAR which you can then use in multiple builds and share with others. Generally, this JAR might include some custom plugins, or bundle several related task classes into a single library. Or some combination of the two.

在我们的例子中,为了简单,我们将从在构建脚本中定义任务类开始。然后我们看一下创建一个独立的项目的方式。
In our examples, we will start with the task class in the build script, to keep things simple. Then we will look at creating a standalone project.

57.2. 编写一个简单的任务类

57.2. Writing a simple task class

要实现自定义任务类,请扩展 DefaultTask
To implement a custom task class, you extend DefaultTask.

示例 57.1. 定义一个自定义任务 - Example 57.1. Defining a custom task

build.gradle

class GreetingTask extends DefaultTask {
}

这个任务不执行任何有用的操作,所以我们来添加一些行为。为此,我们向任务添加一个方法,并使用 TaskAction 注解标记它。当任务执行时,Gradle 将调用这个方法。你不必使用方法来定义任务的行为。比如,你可以在任务构造方法中调用 doFirst()doLast() 并传入一个闭包来添加行为。
This task doesn't do anything useful, so let's add some behaviour. To do so, we add a method to the task and mark it with the TaskAction annotation. Gradle will call the method when the task executes. You don't have to use a method to define the behaviour for the task. You could, for instance, call doFirst() or doLast() with a closure in the task constructor to add behaviour.

示例 57.2. 一个 hello world 任务 - Example 57.2. A hello world task

build.gradle

task hello(type: GreetingTask)

class GreetingTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'hello from GreetingTask'
    }
}

gradle -q hello 的输出结果
Output of gradle -q hello

> gradle -q hello
hello from GreetingTask

让我们添加一个属性到任务中,这样我们就可以自定义它。任务只是 POGO,当你声明一个任务时,可以在这个任务对象上设置属性或调用方法。这里我们添加了一个 greeting 属性,并在声明 greeting 任务时设置它的值。
Let's add a property to the task, so we can customize it. Tasks are simply POGOs, and when you declare a task, you can set the properties or call methods on the task object. Here we add a greeting property, and set the value when we declare the greeting task.

示例 57.3. 一个自定义的 hello world 任务 - Example 57.3. A customizable hello world task

build.gradle

// Use the default greeting
task hello(type: GreetingTask)

// Customize the greeting
task greeting(type: GreetingTask) {
    greeting = 'greetings from GreetingTask'
}

class GreetingTask extends DefaultTask {
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}

gradle -q hello greeting 的输出结果
Output of gradle -q hello greeting

> gradle -q hello greeting
hello from GreetingTask
greetings from GreetingTask

57.3. 独立项目

57.3. A standalone project

现在我们将任务移到一个独立的项目中,这样我们就可以发布它,并与他人共享。这个项目只是一个 Groovy 项目,其将生成包含任务类的 JAR。以下是该项目的一个简单的构建脚本。它应用了 Groovy 插件,并将 Gradle API 添加为编译时依赖。
Now we will move our task to a standalone project, so we can publish it and share it with others. This project is simply a Groovy project that produces a JAR containing the task class. Here is a simple build script for the project. It applies the Groovy plugin, and adds the Gradle API as a compile-time dependency.

示例 57.4. 一个自定义任务的构建 - Example 57.4. A build for a custom task

build.gradle

apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

注意:此例子的代码可以在 Gradle 的二进制文件或源码里的 samples/customPlugin/plugin 中看到。
Note: The code for this example can be found at samples/customPlugin/plugin which is in both the binary and source distributions of Gradle.


我们只是按照约定将任务类的源码放在对应的位置。.
We just follow the convention for where the source for the task class should go.

示例 57.5. 自定义任务 - Example 57.5. A custom task

src/main/groovy/org/gradle/GreetingTask.groovy

package org.gradle

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

class GreetingTask extends DefaultTask {
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}

57.3.1. 在另一个项目中使用你的任务类

57.3.1. Using your task class in another project

要在一个构建脚本中使用一个任务类,你需要将这个类添加到构建脚本的类路径中。要做到这一点,请使用《第 59.5 节,“构建脚本的外部依赖”》中所述的 buildscript { } 块。以下示例展示了当包含任务类的 JAR 已发布到本地存储库时,你可以如何操作。
To use a task class in a build script, you need to add the class to the build script's classpath. To do this, you use a buildscript { } block, as described in Section 59.5, “External dependencies for the build script”. The following example shows how you might do this when the JAR containing the task class has been published to a local repository:

示例 57.6. 在另一个项目中使用自定义任务 - Example 57.6. Using a custom task in another project

build.gradle

buildscript {
    repositories {
        maven {
            url uri('../repo')
        }
    }
    dependencies {
        classpath group: 'org.gradle', name: 'customPlugin', version: '1.0-SNAPSHOT'
    }
}

task greeting(type: org.gradle.GreetingTask) {
    greeting = 'howdy!'
}

57.3.2. 为你的任务类编写测试

57.3.2. Writing tests for your task class

你可以使用 ProjectBuilder 类来创建 Project 实例以在测试任务类时使用。
You can use the ProjectBuilder class to create Project instances to use when you test your task class.

示例 57.7. 测试自定义任务 - Example 57.7. Testing a custom task

src/test/groovy/org/gradle/GreetingTaskTest.groovy

class GreetingTaskTest {
    @Test
    public void canAddTaskToProject() {
        Project project = ProjectBuilder.builder().build()
        def task = project.task('greeting', type: GreetingTask)
        assertTrue(task instanceof GreetingTask)
    }
}

57.4. 增量任务

57.4. Incremental tasks

增量任务还是一个实验性 功能。
Incremental tasks are an incubating feature.

从上面所述的实现的引入(在 Gradle 1.6 发布周期的早期)以来,Gradle 社区内部的讨论中就产生了一些出色的想法,将有关任务实现者的更改信息暴露到下面所述内容中。因此,这个功能的 API 几乎可以肯定将在即将推出的版本中进行更改。但是,请对当前的实现做实现,并与 Gradle 社区分享你的经验。
Since the introduction of the implementation described above (early in the Gradle 1.6 release cycle), discussions within the Gradle community have produced superior ideas for exposing the information about changes to task implementors to what is described below. As such, the API for this feature will almost certainly change in upcoming releases. However, please do experiment with the current implementation and share your experiences with the Gradle community.

这个功能孵化过程是 Gradle 功能生命周期的一部分(参见《附录 C,功能的生命周期》),它存在的目的是为了整合早期用户的反馈内容来确保高质量的最终实现。
The feature incubation process, which is part of the Gradle feature lifecycle (see Appendix C, The Feature Lifecycle), exists for this purpose of ensuring high quality final implementation through incorporation of early user feedback.

通过 Gradle,实现一个在所有输入和输出都是最新状态时跳过的任务是非常简单的(参见《第 15.9 节,“跳过最新的任务”)。然而,有时候从上次执行以来,只有少数输入文件发生了更改,而你想避免重新处理所有未修改的输入。这对于将输入文件按1:1的基础转换为输出文件的转换任务来说特别有用。
With Gradle, it's very simple to implement a task that gets skipped when all of it's inputs and outputs are up to date (see Section 15.9, “Skipping tasks that are up-to-date”). However, there are times when only a few input files have changed since the last execution, and you'd like to avoid reprocessing all of the unchanged inputs. This can be particularly useful for a transformer task, that converts input files to output files on a 1:1 basis.

如果你想优化你的构建,以便只处理已过时的输入,你可以使用增量任务执行此操作。
If you'd like to optimise your build so that only out-of-date inputs are processed, you can do so with an incremental task.

57.4.1. 实现增量任务

57.4.1. Implementing an incremental task

对于一个要增量处理输入的任务,它必须包含一个增量任务操作。这是一个任务操作方法,它包含了一个 IncrementalTaskInputs 参数,该参数提示 Gradle 这个操作将只处理那些更改的输入。
For a task to process inputs incrementally, that task must contain an incremental task action. This is a task action method that contains a single IncrementalTaskInputs parameter, which indicates to Gradle that the action will process the changed inputs only.

这个增量任务操作可以提供一个 IncrementalTaskInputs.outOfDate() 操作来处理所有过时的输入文件,以及一个对自上次执行以来已经被删除的文件执行的 IncrementalTaskInputs.removed() 操作。
The incremental task action may supply an IncrementalTaskInputs.outOfDate() action for processing any input file that is out-of-date, and a IncrementalTaskInputs.removed() action that executes for any input file that has been removed since the previous execution.

示例 57.8. 定义增量任务操作 - Example 57.8. Defining an incremental task action

build.gradle

class IncrementalReverseTask extends DefaultTask {
    @InputDirectory
    def File inputDir

    @OutputDirectory
    def File outputDir

    @Input
    def inputProperty

    @TaskAction
    void execute(IncrementalTaskInputs inputs) {
        println inputs.incremental ? "CHANGED inputs considered out of date" : "ALL inputs considered out of date"
        inputs.outOfDate { change ->
            println "out of date: ${change.file.name}"
            def targetFile = new File(outputDir, change.file.name)
            targetFile.text = change.file.text.reverse()
        }

        inputs.removed { change ->
            println "removed: ${change.file.name}"
            def targetFile = new File(outputDir, change.file.name)
            targetFile.delete()
        }
    }
}

注︰ 此示例的代码可以在 Gradle 的二进制及源码分发包的 samples/userguide/tasks/incrementalTask 中找到。
Note: The code for this example can be found at samples/userguide/tasks/incrementalTask which is in both the binary and source distributions of Gradle.


对于像这样的简单转换任务,任务操作只需要对任何过时的输入生成输出文件,并删除任何已移除的输入的输出文件。
For a simple transformer task like this, the task action simply needs to generate output files for any out-of-date inputs, and delete output files for any removed inputs.

一个任务可能只包含一个增量任务操作。
A task may only contain a single incremental task action.

57.4.2. 哪些输入是被视为过时的?

57.4.2. Which inputs are considered out of date?

当 Gradle 有了前一次任务执行的历史,并且从那一次执行之后任务执行的上下文中只有输入有修改,那么 Gradle 就能够确定这个任务需要重新处理哪些文件。在这种情况下,将对已添加的已修改的的输入文件执行 IncrementalTaskInputs.outOfDate() 操作,并且对任何已移除的输入文件执行 IncrementalTaskInputs.removal() 操作。
When Gradle has history of a previous task execution, and the only changes to the task execution context since that execution are to input files, then Gradle is able to determine which input files need to be reprocessed by the task. In this case, the IncrementalTaskInputs.outOfDate() action will be executed for any input file that was added or modified, and the IncrementalTaskInputs.removed() action will be executed for any removed input file.

然而,有许多情况下,Gradle 还是无法确定需要重新处理哪些输入文件。包括比如:
However, there are many cases where Gradle is unable to determine which input files need to be reprocessed. Examples include:

  • 没有自上次执行以来的有效历史记录。
    There is no history available from a previous execution.
  • 你正在使用不同版本的 Gradle 构建。Gradle 目前不使用来自不同版本的任务历史记录。
    You are building with a different version of Gradle. Currently, Gradle does not use task history from a different version.
  • 添加到任务的 upToDateWhen 条件返回 false
    An upToDateWhen criteria added to the task returns false.
  • 自上次执行以来,输入属性已有修改。
    An input property has changed since the previous execution.
  • 自上次执行以来,一有个或多个的输出文件已更改。
    One or more output files have changed since the previous execution.

在以上所述的任一情况下,Gradle 将把所有输入文件视为 outOfDate,然后会对每个输入文件执行 IncrementalTaskInputs.outOfDate() 操作,并且将不再执行 IncrementalTaskInputs.removremoval() 操作。
In any of these cases, Gradle will consider all of the input files to be outOfDate. The IncrementalTaskInputs.outOfDate() action will be executed for every input file, and the IncrementalTaskInputs.removed() action will not be executed at all.

你可以使用 IncrementalTaskInputs.isIncremental() 来检查 Gradle 是否能够确定输入文件的增量更改。
You can check if Gradle was able to determine the incremental changes to input files with IncrementalTaskInputs.isIncremental().

57.4.3. 增量任务的操作

57.4.3. An incremental task in action

考虑到上面的增量任务实现,我们可以通过示例探讨各种更改场景。请注意,各种变化的任务(“updateInputs”,“removeInput”等)仅用于演示:它们通常不会在构建脚本中。
Given the incremental task implementation above, we can explore the various change scenarios by example. Note that the various mutation tasks ('updateInputs', 'removeInput', etc) are only present for demonstration purposes: these would not normally be part of your build script.

首先,考虑到 IncrementalReverseTask 第一次针对一组输入执行。在这种情况下,所有输入都将认为是“过时的”:
First, consider the IncrementalReverseTask executed against a set of inputs for the first time. In this case, all inputs will be considered "out of date":

示例 57.9. 首次运行增量任务 - Example 57.9. Running the incremental task for the first time

build.gradle

task incrementalReverse(type: IncrementalReverseTask) {
    inputDir = file('inputs')
    outputDir = file("$buildDir/outputs")
    inputProperty = project.properties['taskInputProperty'] ?: "original"
}

构建布局
Build layout

incrementalTask/
  build.gradle
  inputs/
    1.txt
    2.txt
    3.txt

gradle -q incrementalReverse 的输出结果
Output of gradle -q incrementalReverse

> gradle -q incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt

当然,当再次执行任务时没有任何修改,那么任务本身是最新的,并且不会向这个任务操作报告任何文件:
Naturally when the task is executed again with no changes, then task itself is up to date and no files are reported to the task action:

示例 57.10. 在输入不变时运行增量任务 - Example 57.10. Running the incremental task with unchanged inputs

gradle -q incrementalReverse 的输出结果
Output of gradle -q incrementalReverse

> gradle -q incrementalReverse

当以某种方式修改了输入文件,或者新增输入文件时,重新执行这个任务会使这些文件被上报到 IncrementalTaskInputs.outOfDate()
When an input file is modified in some way or a new input file is added, then re-executing the task results in those files being reported to IncrementalTaskInputs.outOfDate():

示例 57.11. 有输入文件更新时运行增量任务 - Example 57.11. Running the incremental task with updated input files

build.gradle

task updateInputs() << {
    file('inputs/1.txt').text = "Changed content for existing file 1."
    file('inputs/4.txt').text = "Content for new file 4."
}

gradle -q updateInputs incrementalReverse 的输出结果
Output of gradle -q updateInputs incrementalReverse

> gradle -q updateInputs incrementalReverse
CHANGED inputs considered out of date
out of date: 1.txt
out of date: 4.txt

当删除一个现有的输入文件后,重新执行这个任务会使该文件被报告给 IncrementalTaskInputs.removal()
When an existing input file is removed, then re-executing the task results that file being reported to IncrementalTaskInputs.removed():

示例 57.12. 当输入文件被删除时运行增量任务 - Example 57.12. Running the incremental task with an input file removed

build.gradle

task removeInput() << {
    file('inputs/3.txt').delete()
}

gradle -q removeInput incrementalReverse 的输出结果
Output of gradle -q removeInput incrementalReverse

> gradle -q removeInput incrementalReverse
CHANGED inputs considered out of date
removed: 3.txt

当删除(或修改)输出文件时,Gradle 无法确定哪些输入文件是已过时的。在这种情况下,所有的 输入文件都会被报告给 IncrementalTaskInputs.outOfDate() 操作,并且不会向 IncrementalTaskInputs.removal() 操作报告任何输入文件:
When an output file is deleted (or modified), then Gradle is unable to determine which input files are out of date. In this case, all input files are reported to the IncrementalTaskInputs.outOfDate() action, and no input files are reported to the IncrementalTaskInputs.removed() action:

示例 57.13. 当输出文件被删除时运行增量任务 - Example 57.13. Running the incremental task with an output file removed

build.gradle

task removeOutput() << {
    file("$buildDir/outputs/1.txt").delete()
}

gradle -q removeOutput incrementalReverse 的输出结果
Output of gradle -q removeOutput incrementalReverse

> gradle -q removeOutput incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt

当修改任务的输入属性时,Gradle 无法确定该属性对任务的输出会有怎样的影响,因此将假定所有输入文件都已过时。所以与更改输出文件的示例类似,所有的输入文件将报告给 IncrementalTaskInputs.outOfDate() 操作,并且不会向 IncrementalTaskInputs.removal() 操作报告任何输入文件:
When a task input property modified, Gradle is not able to determine how this property impacted the task outputs, so all input files are assumed to be out of date. So similar to the changed output file example, all input files are reported to the IncrementalTaskInputs.outOfDate() action, and no input files are reported to the IncrementalTaskInputs.removed() action:

示例 57.14. 当输入属性被更改时运行增量任务 - Example 57.14. Running the incremental task with an input property changed

gradle -q -PtaskInputProperty=changed incrementalReverse 的输出结果
Output of gradle -q -PtaskInputProperty=changed incrementalReverse

> gradle -q -PtaskInputProperty=changed incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt