Gradle支持两种类型的任务. 一种这样的类型是简单任务,您可以在其中用动作关闭定义任务. 我们已经在Build Script Basics中看到了这些. 对于这种类型的任务,动作关闭确定任务的行为. 这种类型的任务非常适合在构建脚本中实施一次性任务.

任务的另一种类型是增强型任务,其中行为内置于任务中,并且该任务提供了一些可用于配置行为的属性. 我们已经在创作任务中看到了这些. 大多数Gradle插件使用增强的任务. 使用增强型任务,您不需要像处理简单任务那样实现任务行为. 您只需声明任务并使用其属性配置任务. 通过这种方式,增强的任务使您可以在许多不同的地方(可能跨不同的构建)重用某种行为.

增强型任务的行为和属性由任务的类定义. 声明增强的任务时,可以指定任务的类型或类.

在Gradle中实现自己的自定义任务类很容易. 您可以使用任何您喜欢的语言来实现自定义任务类,只要最终将其编译为JVM字节码即可. 在我们的示例中,我们将使用Groovy作为实现语言. Groovy,Java或Kotlin都是用于实现任务类的语言,都是不错的选择,因为Gradle API被设计为与这些语言很好地兼容. 通常,使用Java或Kotlin(静态类型)实现的任务将比使用Groovy实现的任务执行得更好.

Packaging a task class

您可以在几个地方放置任务类的源代码.

Build script

您可以将任务类直接包含在构建脚本中. 这样的好处是,任务类将自动编译并包含在构建脚本的类路径中,而无需执行任何操作. 但是,任务类在构建脚本之外不可见,因此您不能在定义该任务的构建脚本之外重用该任务类.

buildSrc project

您可以将任务类的源放在rootProjectDir /buildSrc/src/main/groovy目录(或rootProjectDir /buildSrc/src/main/javarootProjectDir /buildSrc/src/main/kotlin具体取决于您使用的语言). Gradle将负责编译和测试任务类,并使其在构建脚本的类路径中可用. 任务类对于构建所使用的每个构建脚本都是可见的. 但是,它在构建外部不可见,因此您无法在定义该构建的外部重用任务类.使用buildSrc项目方法将任务声明(即任务应执行的操作)与任务实现分开.也就是说,任务是如何完成的.

有关buildSrc项目的更多详细信息,请参见组织Gradle项目.

Standalone project

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.

在我们的示例中,我们将从构建脚本中的任务类开始,以使事情变得简单. 然后,我们将考虑创建一个独立的项目.

Writing a simple task class

要实现自定义任务类,可以扩展DefaultTask .

例子1.定义一个自定义任务
build.gradle
class GreetingTask extends DefaultTask {
}
build.gradle.kts
open class GreetingTask : DefaultTask() {
}

该任务没有做任何有用的事情,因此让我们添加一些行为. 为此,我们向任务添加一个方法,并使用TaskAction批注对其进行标记. 任务执行时,Gradle将调用该方法. 您不必使用方法来定义任务的行为. 例如,您可以在任务构造函数中使用闭包调用doFirst()doLast()以添加行为.

例子2.你好世界任务
build.gradle
class GreetingTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'hello from GreetingTask'
    }
}

// Create a task using the task type
task hello(type: GreetingTask)
build.gradle.kts
open class GreetingTask : DefaultTask() {
    @TaskAction
    fun greet() {
        println("hello from GreetingTask")
    }
}

// Create a task using the task type
tasks.register<GreetingTask>("hello")
gradle -q hello输出
> gradle -q hello
hello from GreetingTask

让我们为任务添加一个属性,以便我们可以对其进行自定义. 任务只是POGO,声明任务时,可以在任务对象上设置属性或调用方法. 在这里,我们添加了greeting属性,并在声明greeting任务时设置了值.

例子3.一个可定制的hello world任务
build.gradle
class GreetingTask extends DefaultTask {
    @Input
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}

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

// Customize the greeting
task greeting(type: GreetingTask) {
    greeting = 'greetings from GreetingTask'
}
build.gradle.kts
open class GreetingTask : DefaultTask() {
    @get:Input
    var greeting = "hello from GreetingTask"

    @TaskAction
    fun greet() {
        println(greeting)
    }
}

// Use the default greeting
tasks.register<GreetingTask>("hello")

// Customize the greeting
tasks.register<GreetingTask>("greeting") {
    greeting = "greetings from GreetingTask"
}
gradle -q hello greeting输出
> gradle -q hello greeting
hello from GreetingTask
greetings from GreetingTask

A standalone project

现在,我们将把任务移到一个独立的项目中,以便我们可以发布它并与他人共享它. 这个项目只是一个Groovy项目,它产生一个包含任务类的JAR. 这是该项目的简单构建脚本. 它应用了Groovy插件,并将Gradle API添加为编译时依赖项.

例子4.一个自定义任务的构建
build.gradle
plugins {
    id 'groovy'
}

dependencies {
    implementation gradleApi()
}
build.gradle.kts
plugins {
    groovy
}

dependencies {
    implementation(gradleApi())
}

我们只是遵循任务类源应该放在哪里的约定.

Example: A custom task

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

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

class GreetingTask extends DefaultTask {

    @Input
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}

Using your task class in another project

要在构建脚本中使用任务类,您需要将该类添加到构建脚本的类路径中. 为此,您可以使用buildscript { }块,如构建脚本的外部依赖项中所述 . 以下示例显示了包含任务类的JAR已发布到本地存储库时如何执行此操作:

Example 5. Using a custom task in another project
build.gradle
buildscript {
    repositories {
        maven {
            url = uri(repoLocation)
        }
    }
    dependencies {
        classpath 'org.gradle:customTask:1.0-SNAPSHOT'
    }
}

task greeting(type: org.gradle.GreetingTask) {
    greeting = 'howdy!'
}
build.gradle.kts
buildscript {
    repositories {
        maven {
            url = uri(repoLocation)
        }
    }
    dependencies {
        classpath("org.gradle:customPlugin:1.0-SNAPSHOT")
    }
}

tasks.register<org.gradle.GreetingTask>("greeting") {
    greeting = "howdy!"
}

Writing tests for your task class

您可以使用ProjectBuilder类创建在测试任务类时要使用的Project实例.

Example: Testing a custom task

src/test/groovy/org/gradle/GreetingTaskTest.groovy
class GreetingTaskTest {
    @Test
    void canAddTaskToProject() {
        Project project = ProjectBuilder.builder().build()
        def task = project.task('greeting', type: GreetingTask)
        assertTrue(task instanceof GreetingTask)
    }
}

Incremental tasks

使用Gradle,实现一个在所有输入和输出都是最新的情况下将被跳过的任务非常简单(请参阅" 增量构建" ). 但是,自上次执行以来,有时只有少数输入文件已更改,因此您希望避免重新处理所有未更改的输入. 这对于将输入文件按1:1转换为输出文件的转换器任务特别有用.

如果您想优化构建,以便仅处理过时的输入文件,则可以使用增量任务来完成 .

IncrementalTask​​Inputs API,在5.4之前的Gradle版本中可用. 使用IncrementalTask​​Inputs时 ,只能查询任务输入的所有文件更改. 无法查询各个输入文件属性的更改. 而且,旧的API不能区分增量任务输入和非增量任务输入,因此任务本身需要确定更改的来源. 因此,不建议使用此API,并最终将其删除. 本文记录了新的InputChanges API,它取代了旧的API并解决了其缺点. 如果需要使用旧的API,请查看Gradle 5.3.1用户手册中的文档.

Implementing an incremental task

为了使任务增量处理输入,该任务必须包含增量任务action . 这是一个具有单个InputChanges参数的任务操作方法. 该参数告诉Gradle该操作仅想处理更改的输入. 此外,任务需要使用@Incremental@SkipWhenEmpty声明至少一个增量文件输入属性.

要查询输入文件属性的增量更改,该属性始终需要返回相同的实例. 完成此操作的最简单方法是对此类属性使用以下类型之一: RegularFilePropertyDirectoryPropertyConfigurableFileCollection .

您可以在" 惰性配置"一章中了解有关RegularFilePropertyDirectoryProperty更多信息,尤其是有关使用只读和可配置属性以及惰性文件属性的部分 .

增量任务操作可以使用InputChanges.getFileChanges()来查找给定的基于文件的输入属性(文件类型为RegularFilePropertyDirectoryPropertyConfigurableFileCollection更改了哪些文件. 该方法返回一个FileChanges类型的Iterable ,然后可以查询以下内容:

以下示例演示了具有目录输入的增量任务. 假定目录包含文本文件的集合,然后将它们复制到输出目录,以反转每个文件中的文本. 需要注意的关键事项是inputDir属性的类型,其批注以及操作( execute() )如何使用getFileChanges()处理自上次构建以来实际上已更改的文件的子集. 您还可以查看如果删除了相应的输入文件,则该操作如何删除目标文件:

例子6.定义一个增量任务动作
build.gradle
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()
            }
        }
    }
}
build.gradle.kts
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())
            }
        }
    }
}

如果由于某种原因(例如,通过使用--rerun-tasks运行)非增量执行--rerun-tasks ,则无论先前状态如何,所有文件均报告为ADDED . 在这种情况下,Gradle自动删除先前的输出,因此增量任务仅需要处理给定的文件.

对于像上述示例这样的简单转换器任务,任务操作仅需要为任何过期输入生成输出文件,并为任何已删除输入删除输出文件.

一个任务只能包含一个增量任务动作.

Which inputs are considered out of date?

如果先前有任务执行,并且自执行以来唯一的更改就是增量输入文件属性,则Gradle能够确定需要处理哪些输入文件(增量执行). 在这种情况下, InputChanges.getFileChanges()方法返回添加修改删除的给定属性的所有输入文件的详细信息.

但是,在许多情况下,Gradle无法确定需要处理哪些输入文件(非增量执行). 示例包括:

  • 没有上一次执行的历史记录.

  • 您正在使用其他版本的Gradle进行构建. 当前,Gradle不使用其他版本的任务历史记录.

  • 添加到任务的upToDateWhen条件返回false .

  • 自上次执行以来,输入属性已更改.

  • 自上次执行以来,非增量输入文件属性已更改.

  • 自上次执行以来,一个或多个输出文件已更改.

在所有这些情况下,Gradle会将所有输入文件报告为ADDED ,而getFileChanges()方法将返回组成给定输入属性的所有文件的详细信息.

您可以使用InputChanges.isIncremental()方法检查任务执行是否为增量执行.

An incremental task in action

给出的例子增量任务执行上面 ,通过基于它的一些场景,让我们走.

首先,考虑一个首次针对一组输入执行的IncrementalReverseTask实例. 在这种情况下,所有输入将被视为已添加,如下所示:

例子7.第一次运行增量任务
build.gradle
task incrementalReverse(type: IncrementalReverseTask) {
    inputDir = file('inputs')
    outputDir = file("$buildDir/outputs")
    inputProperty = project.properties['taskInputProperty'] ?: 'original'
}
build.gradle.kts
tasks.register<IncrementalReverseTask>("incrementalReverse") {
    inputDir.set(file("inputs"))
    outputDir.set(file("$buildDir/outputs"))
    inputProperty.set(project.properties["taskInputProperty"] as String? ?: "original")
}
构建布局
.
├── build.gradle
└── inputs
    ├── 1.txt
    ├── 2.txt
    └── 3.txt
gradle -q incrementalReverse输出
> gradle -q incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt

自然地,如果再次执行任务且没有任何更改,则整个任务都是最新的,并且不执行任务动作:

例子8.在输入不变的情况下运行增量任务
gradle incrementalReverse输出
> gradle incrementalReverse
> Task :incrementalReverse UP-TO-DATE

BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date

当以某种方式修改输入文件或添加新的输入文件时,然后重新执行任务将导致这些文件由InputChanges.getFileChanges()返回. 以下示例在运行增量任务之前修改一个文件的内容并添加另一个文件的内容:

示例9.使用更新的输入文件运行增量任务
build.gradle
task updateInputs() {
    doLast {
        file('inputs/1.txt').text = 'Changed content for existing file 1.'
        file('inputs/4.txt').text = 'Content for new file 4.'
    }
}
build.gradle.kts
tasks.register("updateInputs") {
    doLast {
        file("inputs/1.txt").writeText("Changed content for existing file 1.")
        file("inputs/4.txt").writeText("Content for new file 4.")
    }
}
gradle -q updateInputs incrementalReverse输出输入gradle -q updateInputs incrementalReverse
> gradle -q updateInputs incrementalReverse
Executing incrementally
MODIFIED: 1.txt
ADDED: 4.txt
各种变异任务( updateInputsremoveInput等)仅用于演示增量任务的行为. 不应将它们视为您应该在自己的构建脚本中拥有的任务或任务实现类型.

删除现有输入文件后,重新执行任务将导致该文件由InputChanges.getFileChanges()返回为REMOVED . 以下示例在执行增量任务之前删除现有文件之一:

例子10.在删除输入文件的情况下运行增量任务
build.gradle
task removeInput() {
    doLast {
        file('inputs/3.txt').delete()
    }
}
build.gradle.kts
tasks.register("removeInput") {
    doLast {
        file("inputs/3.txt").delete()
    }
}
gradle -q removeInput incrementalReverse输出输入gradle -q removeInput incrementalReverse
> gradle -q removeInput incrementalReverse
Executing incrementally
REMOVED: 3.txt

输出文件被删除(或修改)时,Gradle无法确定哪些输入文件已过期. 在这种情况下, InputChanges.getFileChanges()返回给定属性的所有输入文件的详细信息. 以下示例仅从构建目录中删除输出文件之一,但请注意如何将所有输入文件视为ADDED

示例11.在删除输出文件的情况下运行增量任务
build.gradle
task removeOutput() {
    doLast {
        file("$buildDir/outputs/1.txt").delete()
    }
}
build.gradle.kts
tasks.register("removeOutput") {
    doLast {
        file("$buildDir/outputs/1.txt").delete()
    }
}
Output of gradle -q removeOutput incrementalReverse
> gradle -q removeOutput incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt

我们要讨论的最后一个场景涉及修改基于非文件的输入属性时发生的情况. 在这种情况下,Gradle无法确定属性如何影响任务输出,因此任务将以非增量方式执行. 这意味着给定属性的所有输入文件都由InputChanges.getFileChanges()返回,并且都被视为ADDED . 下面的例子中,项目属性设置taskInputProperty运行时为新值incrementalReverse任务和项目属性用于初始化任务的inputProperty属性,你可以在看本节的第一个例子 . 在这种情况下,您可以期待以下输出:

例子12.在输入属性改变的情况下运行增量任务
gradle -q -PtaskInputProperty=changed incrementalReverse输出已gradle -q -PtaskInputProperty=changed incrementalReverse
> gradle -q -PtaskInputProperty=changed incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt

Storing incremental state for cached tasks

使用Gradle的InputChanges并不是创建自上次执行以来仅对更改起作用的任务的唯一方法. 诸如Kotlin编译器之类的工具将增量性作为内置功能提供. 通常,此方法的实现方式是该工具将有关先前执行状态的分析数据存储在某个文件中. 如果此类状态文件可重定位 ,则可以将其声明为任务的输出. 这样,当从缓存加载任务的结果时,下一次执行也可以使用从缓存加载的分析数据.

但是,如果状态文件不可重定位,则无法通过构建缓存共享它们. 确实,从高速缓存加载任务时,必须清除所有此类状态文件,以防止过时的状态在下一次执行期间使工具混乱. 如果通过task.localState.register()声明了陈旧文件,或者使用@LocalState批注标记了属性,则Gradle可以确保删除这些陈旧文件.

Declaring and Using Command Line Options

有时,用户希望在命令行而不是构建脚本上声明公开的任务属性的值. 如果更频繁地更改属性值,则在命令行中传递它们特别有用. 任务API支持一种用于标记属性的机制,以在运行时自动生成具有特定名称的相应命令行参数.

Declaring a command-line option

为任务属性公开新的命令行选项非常简单. 您只需要使用Option注释属性的相应setter方法即可. 一个选项需要一个强制标识符. 此外,您可以提供可选的描述. 任务可以公开与类中可用属性一样多的命令行选项.

让我们看一个示例来说明功能. 定制任务UrlVerify通过进行HTTP调用并检查响应代码来验证是否可以解析给定的URL. 可通过属性url配置要验证的url . 该属性的setter方法使用@Option注释.

Example: Declaring a command line option

UrlVerify.java
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选项,可以将为任务声明的所有选项呈现为控制台输出 .

Using an option on the command line

在命令行上使用选项必须遵守以下规则:

  • 该选项使用双破折号作为前缀,例如--url . 单破折号不符合任务选项的有效语法.

  • 选项参数紧随任务声明之后,例如verifyUrl --url=http://www.google.com/ .

  • 任务名称后,可以在命令行中以任意顺序声明任务的多个选项.

回到上一个示例,构建脚本将创建UrlVerify类型的任务实例,并通过暴露选项从命令行提供一个值.

例子13.使用命令行选项
build.gradle
task verifyUrl(type: UrlVerify)
build.gradle.kts
tasks.register<UrlVerify>("verifyUrl")
gradle -q verifyUrl --url=http://www.google.com/输出
> gradle -q verifyUrl --url=http://www.google.com/
Verifying URL 'http://www.google.com/'

Supported data types for options

Gradle限制了可用于声明命令行选项的数据类型集. 命令行上的用法因类型而异.

boolean, Boolean, Property<Boolean>

描述值为truefalse的选项. 在命令行上传递选项会将值视为true . 例如--enabled等于true . 没有该选项将使用属性的默认值.

String, Property<String>

描述具有任意String值的选项. 在命令行上传递选项还需要一个值,例如--container-id=2x94held--container-id 2x94held .

enum, Property<enum>

将选项描述为枚举类型. 在命令行中传递该选项还需要一个值,例如--log-level=DEBUG--log-level debug . 该值不区分大小写.

List<String>, List<enum>

Describes an option that can takes multiple values of a given type. The values for the option have to be provided as multiple declarations e.g. --image-id=123 --image-id=456. Other notations such as comma-separated lists or multiple values separated by a space character are currently not supported.

Documenting available values for an option

理论上,属性类型为StringList<String>可以接受任何任意值. 可以在注释OptionValues的帮助下以编程方式记录此选项的期望值. 可以将此注释分配给任何返回受支持数据类型之一的List的方法. 另外,您必须提供选项标识符以指示选项和可用值之间的关系.

在选项中不支持的命令行上传递值不会使构建失败或引发异常. 您必须在任务操作中为此类行为实现自定义逻辑.

本示例说明了单个任务使用多个选项的情况. 任务实现提供选项output-type的可用值列表.

Example: Declaring available values for an option

UrlProcess.java
import org.gradle.api.tasks.options.Option;
import org.gradle.api.tasks.options.OptionValues;

public class UrlProcess extends DefaultTask {
    private String url;
    private OutputType outputType;

    @Option(option = "url", description = "Configures the URL to be write to the output.")
    public void setUrl(String url) {
        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
    }
}

Listing command line options

使用注释OptionOptionValues的命令行选项是自记录的. 您将在help任务的控制台输出中看到声明的选项及其可用值 . 输出以字母顺序呈现选项.

Example: Listing available values for option

gradle -q help --task processUrl输出
> gradle -q help --task processUrl
Detailed task information for processUrl

Path
     :processUrl

Type
     UrlProcess (UrlProcess)

Options
     --output-type     Configures the output type.
                       Available values are:
                            CONSOLE
                            FILE

     --url     Configures the URL to be write to the output.

Description
     -

Group
     -

Limitations

当前支持声明命令行选项有一些限制.

  • 只能通过注释为自定义任务声明命令行选项. 没有用于定义选项的程序化等效项.

  • 选项不能全局声明,例如在项目级别或作为插件的一部分.

  • 在命令行上分配选项时,需要显式地说明公开该选项的任务,例如gradle check --tests abc不起作用,即使check任务取决于test任务也是如此.

The Worker API

Worker API是一个孵化功能.

从对增量任务的讨论中可以看出,任务执行的工作可以看作是离散的单元(即,将输入的子集转换为一定的输出子集). 很多时候,这些工作单元彼此高度独立,这意味着它们可以以任何顺序执行,并且可以简单地汇总在一起以形成任务的整体动作. 在单线程执行中,这些工作单元将按顺序执行,但是,如果我们有多个处理器,则最好同时执行独立的工作单元. 这样,我们可以在构建时充分利用可用资源,并更快地完成任务的活动.

Worker API提供了一种完成此操作的机制. 它允许在任务动作期间安全,并发地执行多项工作. 但是,Worker API的好处不仅限于并行执行任务. 您还可以配置所需的隔离级别,以便可以在隔离的类加载器中甚至在隔离的进程中执行工作. 此外,好处甚至超出了执行单个任务的范围. 默认情况下,使用Worker API,Gradle可以开始并行执行任务. 换句话说,一旦任务提交了要异步执行的工作并退出了任务动作,Gradle便可以并行开始执行其他独立任务,即使这些任务在同一项目中也是如此.

Using the Worker API

为了将工作提交给Worker API,必须提供两件事:工作单元的实现,以及工作单元的参数.

工作单元的参数定义为实现WorkParameters的接口或抽象类. 参数类型必须是托管类型 .

您可以在开发自定义Gradle类型中找到有关实现工作参数的更多信息.

该实现是扩展WorkAction的类. 此类应该是抽象的,并且不应实现getParameters()方法. Gradle将在运行时为每个工作单元的参数对象注入此方法的实现.

例子14.定义工作单位参数和实现
build.gradle
// The parameters for a single unit of work
interface ReverseParameters extends WorkParameters {
    RegularFileProperty getFileToReverse()
    DirectoryProperty getDestinationDir()
}

// The implementation of a single unit of work.
abstract class ReverseFile implements WorkAction<ReverseParameters> {
    private final FileSystemOperations fileSystemOperations

    @Inject
    public ReverseFile(FileSystemOperations fileSystemOperations) {
        this.fileSystemOperations = fileSystemOperations
    }

    @Override
    void execute() {
        fileSystemOperations.copy {
            from parameters.fileToReverse
            into parameters.destinationDir
            filter { String line -> line.reverse() }
        }
    }
}
build.gradle.kts
import javax.inject.Inject

// The parameters for a single unit of work
interface ReverseParameters : WorkParameters {
    val fileToReverse : RegularFileProperty
    val destinationDir : DirectoryProperty
}

// The implementation of a single unit of work
abstract class ReverseFile @Inject constructor(val fileSystemOperations: FileSystemOperations) : WorkAction<ReverseParameters> {
    override fun execute() {
        fileSystemOperations.copy {
            from(parameters.fileToReverse)
            into(parameters.destinationDir)
            filter { line: String -> line.reversed() }
        }
    }
}

WorkAction实现可以注入在工作执行过程中提供功能的服务,例如上例中的FileSystemOperations服务. 有关注入服务类型的更多信息,请参见服务注入 .

为了提交工作单元,必须首先获得WorkerExecutor . 为此,任务应具有一个带有javax.inject.Inject注释的构造函数,该构造函数接受WorkerExecutor参数. 创建任务时,Gradle将在运行时注入WorkerExecutor的实例. 然后可以创建一个WorkQueue对象,并可以提交各个工作项.

例子15.提交工作单元以执行
build.gradle
class ReverseFiles extends SourceTask {
    private final WorkerExecutor workerExecutor

    @OutputDirectory
    File outputDir

    // The WorkerExecutor will be injected by Gradle at runtime
    @Inject
    ReverseFiles(WorkerExecutor workerExecutor) {
        this.workerExecutor = workerExecutor
    }

    @TaskAction
    void reverseFiles() {
        // Create a WorkQueue to submit work items
        WorkQueue workQueue = workerExecutor.noIsolation()

        // 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
            }
        }
    }
}
build.gradle.kts
// The WorkerExecutor will be injected by Gradle at runtime
open class ReverseFiles @Inject constructor(private val workerExecutor: WorkerExecutor) : SourceTask() {
    @OutputDirectory
    lateinit var outputDir: File

    @TaskAction
    fun reverseFiles() {
        // Create a WorkQueue to submit work items
        val workQueue = workerExecutor.noIsolation()

        // Create and submit a unit of work for each file
        source.forEach { file ->
            workQueue.submit(ReverseFile::class) {
                fileToReverse.set(file)
                destinationDir.set(outputDir)
            }
        }
    }
}

提交任务动作的所有工作后,可以安全地退出任务动作. 该工作将异步和并行执行(取决于max-workers的设置). 当然,在完成所有异步工作之前,任何依赖于此任务的任务(以及该任务的任何后续任务动作)都不会开始执行. 但是,与此任务无关的其他独立任务可以立即开始执行.

如果在执行异步工作时发生任何故障,该任务将失败,并且将抛出WorkerExecutionException,详细说明每个失败的工作项的故障. 这将被视为任务执行期间的任何失败,并且将阻止执行任何从属任务.

但是,在某些情况下,可能希望在退出任务操作之前等待工作完成. 使用WorkQueue.await()方法可以实现. 如在使工件异步完成的情况下,在执行工作的项目中发生的任何故障将被浮出水面作为WorkerExecutionException从抛出WorkQueue.await()方法.

请注意,当任务退出任务动作并将执行控制权返回给Gradle时,Gradle将仅开始并行运行其他独立任务. 使用WorkQueue.await()时 ,执行不会离开任务动作. 这意味着Gradle将不允许其他任务开始执行,并且将等待任务动作完成后再执行.

例子16.等待异步工作完成
build.gradle
        // Create a WorkQueue to submit work items
        WorkQueue workQueue = workerExecutor.noIsolation()

        // 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
            }
        }

        // Wait for all asynchronous work submitted to this queue to complete before continuing
        workQueue.await()
        logger.lifecycle("Created ${outputDir.listFiles().size()} reversed files in ${projectLayout.projectDirectory.asFile.relativePath(outputDir)}")
build.gradle.kts
        // Create a WorkQueue to submit work items
        val workQueue = workerExecutor.noIsolation()

        // Create and submit a unit of work for each file
        source.forEach { file ->
            workQueue.submit(ReverseFile::class) {
                fileToReverse.set(file)
                destinationDir.set(outputDir)
            }
        }

        // Wait for all asynchronous work submitted to this queue to complete before continuing
        workQueue.await()
        logger.lifecycle("Created ${outputDir.listFiles().size} reversed files in ${outputDir.toRelativeString(projectLayout.projectDirectory.asFile)}")

Isolation Modes

摇篮提供了可以在创建时被配置三种隔离模式工作队列上使用下列方法中的一个被指定和WorkerExecutor

WorkerExecutor.noIsolation()

这表明工作应在具有最小隔离度的线程中运行. 例如,它将共享从中加载任务的同一类加载器. 这是最快的隔离级别.

WorkerExecutor.classLoaderIsolation()

这表明工作应在具有隔离类加载器的线程中运行. 类加载器将具有来自加载了工作单元实现类的类加载器的类路径,以及通过ClassLoaderWorkerSpec.getClasspath()添加的任何其他类路径条目.

WorkerExecutor.processIsolation()

这表明应通过在单独的进程中执行工作来最大程度地隔离工作. 流程的类加载器将使用来自加载了工作单元的类加载器的类路径,以及通过ClassLoaderWorkerSpec.getClasspath()添加的任何其他类路径条目. 此外,该过程将是一个工作守护程序 ,该守护程序将保持活动状态,并且可以重复用于将来可能具有相同要求的工作项. 可以使用ProcessWorkerSpec.forkOptions(org.gradle.api.Action)使用与Gradle JVM不同的设置来配置此过程.

Worker Daemons

使用processIsolation() ,gradle将启动一个寿命很长的Worker Daemon进程,该进程可用于将来的工作项.

例子17.提交要在worker守护进程中运行的工作项
build.gradle
        // 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
            }
        }
build.gradle.kts
        // 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.set(file)
                destinationDir.set(outputDir)
            }
        }

提交工作程序守护程序的工作单元时,Gradle首先会查看是否存在兼容的空闲守护程序. 如果是这样,它将把工作单元发送到空闲的守护程序,将其标记为忙. 如果没有,它将启动一个新的守护程序. 在评估兼容性时,Gradle会考虑许多标准,所有这些标准都可以通过ProcessWorkerSpec.forkOptions(org.gradle.api.Action)进行控制.

默认情况下,辅助守护程序的最大堆启动为512MB. 可以通过调整worker fork选项来更改.

executable

守护程序仅在使用相同的Java可执行文件时才被认为是兼容的.

classpath

如果守护程序的类路径包含所有请求的类路径条目,则该守护程序被认为是兼容的. 请注意,只有当类路径与请求的类路径完全匹配时,守护程序才被认为是兼容的.

heap settings

如果守护程序至少具有与请求相同的堆大小设置,则认为该守护程序是兼容的. 换句话说,具有高于请求的堆设置的守护程序将被视为兼容.

jvm arguments

如果守护程序已设置所有请求的jvm参数,则认为该守护程序兼容. 请注意,如果守护程序除了请求的参数之外还具有其他jvm参数,则该守护程序被认为是兼容的(除了专门处理的参数,例如堆设置,断言,调试等).

system properties

如果守护程序已将请求的所有系统属性设置为相同的值,则该守护程序被认为是兼容的. 请注意,如果守护程序具有除请求的属性之外的其他系统属性,则该守护程序被认为是兼容的.

environment variables

如果守护程序已将所有请求的环境变量设置为相同的值,则该守护程序被认为是兼容的. 请注意,如果守护程序除请求的环境变量之外还具有更多环境变量,则该守护程序被认为是兼容的.

bootstrap classpath

如果守护程序包含所有请求的引导类路径条目,则该守护程序被认为是兼容的. 请注意,如果守护程序除请求的引导程序类路径条目之外还有更多的引导程序类路径条目,则该守护程序被视为兼容.

debug

仅当将debug设置为与请求相同的值(真或假)时,守护程序才被视为兼容.

enable assertions

仅当启用断言设置为与请求相同的值(true或false)时,守护程序才被视为兼容.

default character encoding

仅当默认字符编码设置为与请求相同的值时,守护程序才被视为兼容.

Worker daemons will remain running until either the build daemon that started them is stopped, or system memory becomes scarce. When available system memory is low, Gradle will begin stopping worker daemons in an attempt to minimize memory consumption.

Cancellation and timeouts

为了支持取消操作(例如,当用户使用CTRL + C停止构建时)和任务超时,自定义任务应对被中断的执行线程做出反应. 通过工作人员API提交的工作项也是如此. 如果任务在10秒内未响应中断,则守护程序将关闭以释放系统资源.

More details

在自定义Gradle插件中打包自定义任务类型通常是一种好方法. 该插件可以为任务类型提供有用的默认值和约定,并提供一种从构建脚本或其他插件中使用任务类型的便捷方法. 请参阅开发自定义Gradle插件以获取更多详细信息.

Gradle provides a number of features that are helpful when developing Gradle types, including tasks. Please see Developing Custom Gradle Types for more details.