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

您可以为任务类创建一个单独的项目. 这个项目产生并发布了一个JAR,您可以在多个版本中使用它并与他人共享. 通常,此JAR可能包含一些自定义插件,或将几个相关的任务类捆绑到一个库中. 或两者的某种组合.

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

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()
    implementation localGroovy()
}
build.gradle.kts
plugins {
    groovy
}

dependencies {
    implementation(gradleApi())
    implementation(localGroovy())
}
该示例的代码可以在Gradle的'-all'发行版的samples/customPlugin中找到.

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

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

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

    @TaskAction
    def greet() {
        println greeting
    }
}

Using your task class in another project

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

例子5.在另一个项目中使用自定义任务
build.gradle
buildscript {
    repositories {
        maven {
            url = uri(repoLocation)
        }
    }
    dependencies {
        classpath 'org.gradle:customPlugin: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
    public 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 ,然后可以查询以下内容:

The following example demonstrates an incremental task that has a directory input. It assumes that the directory contains a collection of text files and copies them to an output directory, reversing the text within each file. The key things to note are the type of the inputDir property, its annotations, and how the action (execute()) uses getFileChanges() to process the subset of files that have actually changed since the last build. You can also see how the action deletes a target file if the corresponding input file has been removed:

例子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())
            }
        }
    }
}
该示例的代码可以在Gradle的" -all"分发版中的samples/userguide/tasks/incrementalTask中找到.

如果由于某种原因(例如,通过使用--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

Example 11. Running the incremental task with an output file removed
build.gradle
task removeOutput() {
    doLast {
        file("$buildDir/outputs/1.txt").delete()
    }
}
build.gradle.kts
tasks.register("removeOutput") {
    doLast {
        file("$buildDir/outputs/1.txt").delete()
    }
}
gradle -q removeOutput incrementalReverse输出输出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 . 单破折号不符合任务选项的有效语法.

  • The option argument follows directly after the task declaration e.g. 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>

描述可以接受给定类型的多个值的选项. 该选项的值必须作为多个声明提供,例如--image-id=123 --image-id=456 . 当前不支持其他表示法,例如以逗号分隔的列表或由空格字符分隔的多个值.

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() }
        }
    }
}

A WorkAction implementation can inject services that provide capabilities during work execution, such as the FileSystemOperations service in the example above. See Service Injection for further information on injecting service types.

为了提交工作单元,必须首先获得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)
            }
        }
    }
}

Once all of the work for a task action has been submitted, it is safe to exit the task action. The work will be executed asynchronously and in parallel (up to the setting of max-workers). Of course, any tasks that are dependent on this task (and any subsequent task actions of this task) will not begin executing until all of the asynchronous work completes. However, other independent tasks that have no relationship to this task can begin executing immediately.

如果在执行异步工作时发生任何故障,则该任务将失败,并且将抛出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 ${project.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 ${project.relativePath(outputDir)}")

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)进行控制.

executable

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

classpath

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

heap settings

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

jvm arguments

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

system properties

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

environment variables

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

bootstrap classpath

A daemon is considered compatible if it contains all of the bootstrap classpath entries requested. Note that a daemon is considered compatible if it has more bootstrap classpath entries in addition to those requested.

debug

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

enable assertions

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

default character encoding

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

辅助守护程序将保持运行状态,直到启动它们的构建守护程序停止或系统内存不足为止. 当可用系统内存不足时,Gradle将开始停止工作程序守护程序,以尽量减少内存消耗.

Cancellation and timeouts

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

More details

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

Gradle提供了许多功能,这些功能在开发Gradle类型(包括任务)时很有用. 有关更多详细信息,请参见开发自定义Gradle类型 .