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

Chapter 57. Writing Custom Task Classes

Gradle 支持两种类型的任务。一种是简单的任务,你可以使用一个action闭包来定义它。这样的任务我们在第六章, 构建脚本基础已经看到过。对于这种类型的任务,action闭包决定了这个任务的行为。这个类型的任务适合于实现在构建脚本中只执行一次的任务。
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/groovy 目录中。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

让我们添加一个属性到这个任务中,这样我们就可以自定义它。任务是简单的POGOs,当你声明一个任务时,你可以在这个任务对象上设置属性或者是调用方法。这里我们添加了一个 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,实现一个当输入和输出都是up to date时自动跳过的任务是非常轻松的(参见第 15.9节,“跳过 up-to-date 的任务”)。然而,有时候从上次执行以来,只有少量输入文件被修改了,而你想要避免重新处理所有未修改的输入。对于将输入文件按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.

如果你想优化你的构建,以便只有out-of-date的输入被处理,你可以使用一个增量任务来做。
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() 操作,用于处理所有out-of-date的输入文件,以及一个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.


对于像这样的简单转换任务,任务操作只需要对任何out-of-date的输入生成输出的文件,并对任何已移除的输入删除对应的输出文件。
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. 哪些输入是被视为out-of-date的?

57.4.2. Which inputs are considered out of date?

当Gradle有了前一次任务执行的历史,并且从那一次执行开始任务执行的上下文中只有输入文件有更改,那么Gradle 就能够决定哪些输入文件需要被这个任务重新处理。在这种情况下,IncrementalTaskInputs.outOfDate() 操作会对任何新加入的 或者是 已修改的输入文件执行,并且IncrementalTaskInputs.removed()操作会对任何被移除的输入文件执行。
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.removed()操作。
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第一次针对一组输入执行。在这种情况下,所有的输入都被认为是“out of date”:
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

当然,对于没有任何修改的情况下执行任务时,该任务本身是up to date的,并且不会有文件上报到这个任务操作中:
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

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是无法确定哪一个输入文件是out of date的。在这种情况下,所有的输入文件都会被报告到 IncrementalTaskInputs.outOfDate()操作中,并且没有文件会被上报到 IncrementalTaskInputs.removed() 操作:
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 无法确定这个属性是如何影响到这个任务的输出的,因此所有的输入文件都会被认为是out of date的。所以类似于更改输出文件的例子,所有的输入文件都会被报告到 IncrementalTaskInputs.outOfDate()操作中,并且没有文件会被上报到 IncrementalTaskInputs.removed() 操作:
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