任务依赖关系允许任务根据其依赖关系按特定顺序执行。这确保了依赖于其他任务的任务只在其依赖项完成后才执行。

writing tasks 2

任务依赖关系可分为隐式显式

隐式依赖关系

这些依赖关系是 Gradle 根据任务的操作和配置自动推断出来的。例如,如果 taskB 使用 taskA 的输出(例如 taskA 生成的文件),Gradle 将自动确保 taskAtaskB 之前执行,以满足此依赖关系。

显式依赖关系

这些依赖关系使用 dependsOnmustRunAftershouldRunAfter 方法在构建脚本中显式声明。例如,如果您想确保 taskB 始终在 taskA 之后运行,可以使用 taskB.mustRunAfter(taskA) 显式声明此依赖关系。

隐式和显式依赖关系都在定义任务执行顺序和确保任务按正确序列执行以产生所需构建输出方面发挥着关键作用。

任务依赖关系

Gradle 本身理解任务之间的依赖关系。因此,当您指定某个任务时,它可以确定需要执行哪些任务。

让我们以一个包含 app 子项目和 some-logic 子项目的示例应用程序为例

settings.gradle.kts
rootProject.name = "gradle-project"
include("app")
include("some-logic")
settings.gradle
rootProject.name = 'gradle-project'
include('app')
include('some-logic')

假设 app 子项目依赖于名为 some-logic 的子项目,该子项目包含一些 Java 代码。我们在 app 构建脚本中添加此依赖关系

app/build.gradle.kts
plugins {
    id("application")                       // app is now a java application
}

application {
    mainClass.set("hello.HelloWorld")       // main class name required by the application plugin
}

dependencies {
    implementation(project(":some-logic"))  // dependency on some-logic
}
app/build.gradle
plugins {
    id('application')                       // app is now a java application
}

application {
    mainClass = 'hello.HelloWorld'          // main class name required by the application plugin
}

dependencies {
    implementation(project(':some-logic'))  // dependency on some-logic
}

如果我们再次运行 :app:build,我们会看到 some-logic 的 Java 代码也会被 Gradle 自动编译。

$./gradlew :app:build

> Task :app:processResources NO-SOURCE
> Task :app:processTestResources NO-SOURCE
> Task :some-logic:compileJava UP-TO-DATE
> Task :some-logic:processResources NO-SOURCE
> Task :some-logic:classes UP-TO-DATE
> Task :some-logic:jar UP-TO-DATE
> Task :app:compileJava
> Task :app:classes
> Task :app:jar UP-TO-DATE
> Task :app:startScripts
> Task :app:distTar
> Task :app:distZip
> Task :app:assemble
> Task :app:compileTestJava UP-TO-DATE
> Task :app:testClasses UP-TO-DATE
> Task :app:test
> Task :app:check
> Task :app:build

BUILD SUCCESSFUL in 430ms
9 actionable tasks: 5 executed, 4 up-to-date

添加依赖关系

您可以通过多种方式定义任务的依赖关系。

使用任务名称和 dependsOn()` 方法定义依赖关系是最简单的方法。

以下示例展示了如何从 taskX 添加依赖关系到 taskY

tasks.register("taskX") {
    dependsOn("taskY")
}
tasks.register("taskX") {
    dependsOn "taskY"
}
$ gradle -q taskX
taskY
taskX

有关任务依赖关系的更多信息,请参阅 Task API。

任务排序

在某些情况下,控制两个任务的执行顺序很有用,而无需在这些任务之间引入显式依赖关系。

任务排序和任务依赖关系之间的主要区别在于,排序规则不影响执行哪些任务,只影响它们的执行顺序。

任务排序在多种场景下非常有用

  • 强制任务的顺序执行(例如,build 绝不能在 clean 之前运行)。

  • 在构建早期运行构建验证(例如,在开始发布构建工作之前验证是否拥有正确的凭据)。

  • 通过在长时间验证任务之前运行快速验证任务来更快地获得反馈(例如,单元测试应该在集成测试之前运行)。

  • 汇总所有特定类型任务结果的任务(例如,测试报告任务合并所有已执行测试任务的输出)。

提供了两种排序规则:“必须在之后运行”和“应该在之后运行”。

要指定两个任务之间的“必须在之后运行”或“应该在之后运行”排序,您可以使用 Task.mustRunAfter(java.lang.Object...)Task.shouldRunAfter(java.lang.Object...) 方法。这些方法接受任务实例、任务名称或 Task.dependsOn(java.lang.Object...) 接受的任何其他输入。

当您使用“必须在之后运行”时,您指定了当构建需要执行 taskXtaskY 时,taskY 必须始终在 taskX 之后运行。因此,如果您只运行带有 mustRunAftertaskY,它不会导致 taskX 运行。这表示为 taskY.mustRunAfter(taskX)

build.gradle.kts
val taskX by tasks.registering {
    doLast {
        println("taskX")
    }
}
val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}
taskY {
    mustRunAfter(taskX)
}
build.gradle
def taskX = tasks.register('taskX') {
    doLast {
        println 'taskX'
    }
}
def taskY = tasks.register('taskY') {
    doLast {
        println 'taskY'
    }
}
taskY.configure {
    mustRunAfter taskX
}
$ gradle -q taskY taskX
taskX
taskY

“应该在之后运行”排序规则类似但不那么严格,因为它在两种情况下会被忽略

  1. 如果使用该规则引入了排序循环。

  2. 在使用并行执行时,除了“应该在之后运行”的任务外,所有任务依赖关系都已满足,那么无论其“应该在之后运行”的依赖关系是否已运行,该任务都会运行。

在排序有帮助但不严格要求的情况下,您应该使用“应该在之后运行”

build.gradle.kts
val taskX by tasks.registering {
    doLast {
        println("taskX")
    }
}
val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}
taskY {
    shouldRunAfter(taskX)
}
build.gradle
def taskX = tasks.register('taskX') {
    doLast {
        println 'taskX'
    }
}
def taskY = tasks.register('taskY') {
    doLast {
        println 'taskY'
    }
}
taskY.configure {
    shouldRunAfter taskX
}
$ gradle -q taskY taskX
taskX
taskY

在上面的示例中,仍然可以在不导致 taskX 运行的情况下执行 taskY

$ gradle -q taskY
taskY

如果“应该在之后运行”排序规则引入排序循环,它将被忽略

build.gradle.kts
val taskX by tasks.registering {
    doLast {
        println("taskX")
    }
}
val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}
val taskZ by tasks.registering {
    doLast {
        println("taskZ")
    }
}
taskX { dependsOn(taskY) }
taskY { dependsOn(taskZ) }
taskZ { shouldRunAfter(taskX) }
build.gradle
def taskX = tasks.register('taskX') {
    doLast {
        println 'taskX'
    }
}
def taskY = tasks.register('taskY') {
    doLast {
        println 'taskY'
    }
}
def taskZ = tasks.register('taskZ') {
    doLast {
        println 'taskZ'
    }
}
taskX.configure { dependsOn(taskY) }
taskY.configure { dependsOn(taskZ) }
taskZ.configure { shouldRunAfter(taskX) }
$ gradle -q taskX
taskZ
taskY
taskX

请注意,taskY.mustRunAfter(taskX)taskY.shouldRunAfter(taskX) 并不意味着任务之间存在任何执行依赖关系

  • 可以独立执行 taskXtaskY。排序规则仅在两个任务都被调度执行时才生效。

  • 当使用 --continue 运行时,如果 taskX 失败,taskY 可能仍然执行。

Finalizer 任务

当 finalized 任务被调度运行时,Finalizer 任务会自动添加到任务图中。

要指定 finalizer 任务,请使用 Task.finalizedBy(java.lang.Object…​) 方法。此方法接受任务实例、任务名称或 Task.dependsOn(java.lang.Object…​) 接受的任何其他输入

build.gradle.kts
val taskX by tasks.registering {
    doLast {
        println("taskX")
    }
}
val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}

taskX { finalizedBy(taskY) }
build.gradle
def taskX = tasks.register('taskX') {
    doLast {
        println 'taskX'
    }
}
def taskY = tasks.register('taskY') {
    doLast {
        println 'taskY'
    }
}

taskX.configure { finalizedBy taskY }
$ gradle -q taskX
taskX
taskY

即使 finalized 任务失败或被视为 UP-TO-DATE,Finalizer 任务也会执行。

build.gradle.kts
val taskX by tasks.registering {
    doLast {
        println("taskX")
        throw RuntimeException()
    }
}
val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}

taskX { finalizedBy(taskY) }
build.gradle
def taskX = tasks.register('taskX') {
    doLast {
        println 'taskX'
        throw new RuntimeException()
    }
}
def taskY = tasks.register('taskY') {
    doLast {
        println 'taskY'
    }
}

taskX.configure { finalizedBy taskY }
$ gradle -q taskX
taskX
taskY

FAILURE: Build failed with an exception.

* Where:
Build file '/home/user/gradle/samples/build.gradle' line: 4

* What went wrong:
Execution failed for task ':taskX'.
> java.lang.RuntimeException (no error message)

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 0s

Finalizer 任务在构建创建了无论构建失败还是成功都必须清理的资源时非常有用。例如,在集成测试任务之前启动的 Web 容器,即使某些测试失败,也必须将其关闭。

跳过任务

Gradle 提供了多种方法来跳过任务的执行。

1. 使用断言

您可以使用 Task.onlyIf 为任务附加断言。任务的操作只有在该断言评估为 true 时才会执行。

断言作为参数传递给任务,如果任务将执行则返回 true,如果任务将被跳过则返回 false。断言在任务执行之前进行评估。

onlyIf() 传递一个可选的原因字符串有助于解释任务被跳过的原因

build.gradle.kts
val hello by tasks.registering {
    doLast {
        println("hello world")
    }
}

hello {
    val skipProvider = providers.gradleProperty("skipHello")
    onlyIf("there is no property skipHello") {
        !skipProvider.isPresent()
    }
}
build.gradle
def hello = tasks.register('hello') {
    doLast {
        println 'hello world'
    }
}

hello.configure {
    def skipProvider = providers.gradleProperty("skipHello")
    onlyIf("there is no property skipHello") {
        !skipProvider.present
    }
}
$ gradle hello -PskipHello
> Task :hello SKIPPED

BUILD SUCCESSFUL in 0s

要了解任务被跳过的原因,请使用 --info 日志级别运行构建。

$ gradle hello -PskipHello --info
...

> Task :hello SKIPPED
Skipping task ':hello' as task onlyIf 'there is no property skipHello' is false.
:hello (Thread[included builds,5,main]) completed. Took 0.018 secs.

BUILD SUCCESSFUL in 13s

2. 使用 StopExecutionException

如果跳过任务的逻辑无法用断言表达,您可以使用 StopExecutionException

如果操作抛出此异常,则该任务操作以及后续任何操作的执行都将被跳过。构建将继续执行下一个任务

build.gradle.kts
val compile by tasks.registering {
    doLast {
        println("We are doing the compile.")
    }
}

compile {
    doFirst {
        // Here you would put arbitrary conditions in real life.
        if (true) {
            throw StopExecutionException()
        }
    }
}
tasks.register("myTask") {
    dependsOn(compile)
    doLast {
        println("I am not affected")
    }
}
build.gradle
def compile = tasks.register('compile') {
    doLast {
        println 'We are doing the compile.'
    }
}

compile.configure {
    doFirst {
        // Here you would put arbitrary conditions in real life.
        if (true) {
            throw new StopExecutionException()
        }
    }
}
tasks.register('myTask') {
    dependsOn('compile')
    doLast {
        println 'I am not affected'
    }
}
$ gradle -q myTask
I am not affected

如果您使用 Gradle 提供的任务,此功能会很有帮助。它允许您为这类任务的内置操作添加条件执行。[1]

3. 启用和禁用任务

每个任务都有一个 enabled 标志,默认为 true。将其设置为 false 可以阻止执行任务的操作。

被禁用的任务将被标记为 SKIPPED

build.gradle.kts
val disableMe by tasks.registering {
    doLast {
        println("This should not be printed if the task is disabled.")
    }
}

disableMe {
    enabled = false
}
build.gradle
def disableMe = tasks.register('disableMe') {
    doLast {
        println 'This should not be printed if the task is disabled.'
    }
}

disableMe.configure {
    enabled = false
}
$ gradle disableMe
> Task :disableMe SKIPPED

BUILD SUCCESSFUL in 0s

4. 任务超时

每个任务都有一个 timeout 属性,可用于限制其执行时间。当任务达到其超时时间时,其任务执行线程将被中断。该任务将被标记为 FAILED

Finalizer 任务将执行。如果使用 --continue,其他任务将继续运行。

不响应中断的任务无法超时。所有 Gradle 的内置任务都响应超时。

build.gradle.kts
tasks.register("hangingTask") {
    doLast {
        Thread.sleep(100000)
    }
    timeout = Duration.ofMillis(500)
}
build.gradle
tasks.register("hangingTask") {
    doLast {
        Thread.sleep(100000)
    }
    timeout = Duration.ofMillis(500)
}

任务规则

有时您可能希望任务的行为取决于大量或无限范围的参数值。提供此类任务的一种非常棒且富有表现力的方式是任务规则

build.gradle.kts
tasks.addRule("Pattern: ping<ID>") {
    val taskName = this
    if (startsWith("ping")) {
        task(taskName) {
            doLast {
                println("Pinging: " + (taskName.replace("ping", "")))
            }
        }
    }
}
build.gradle
tasks.addRule("Pattern: ping<ID>") { String taskName ->

    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}
$ gradle -q pingServer1
Pinging: Server1

String 参数用作规则的描述,使用 ./gradlew tasks 时会显示该描述。

规则不仅在从命令行调用任务时使用。您还可以在基于规则的任务上创建 dependsOn 关系

build.gradle.kts
tasks.addRule("Pattern: ping<ID>") {
    val taskName = this
    if (startsWith("ping")) {
        task(taskName) {
            doLast {
                println("Pinging: " + (taskName.replace("ping", "")))
            }
        }
    }
}

tasks.register("groupPing") {
    dependsOn("pingServer1", "pingServer2")
}
build.gradle
tasks.addRule("Pattern: ping<ID>") { String taskName ->

    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}

tasks.register('groupPing') {
    dependsOn 'pingServer1', 'pingServer2'
}
$ gradle -q groupPing
Pinging: Server1
Pinging: Server2

如果您运行 ./gradlew -q tasks,您不会找到名为 pingServer1pingServer2 的任务,但该脚本会根据运行这些任务的请求执行逻辑。

从执行中排除任务

您可以使用 -x--exclude-task 命令行选项并提供要排除的任务名称来排除任务的执行。

$ ./gradlew build -x test

例如,您可以运行 check 任务但排除 test 任务的运行。这种方法可能导致意外结果,特别是当您排除一个可执行任务,而该任务产生的结果是其他任务所必需的。建议不要依赖 -x 参数,而是为所需操作定义一个合适的生命周期任务。

使用 -x 是一种应该避免的做法,尽管仍然普遍存在。


1. 您可能想知道为什么既没有导入 StopExecutionException,也没有通过其完全限定名访问它。原因是 Gradle 会向您的脚本添加一组默认导入(参见 Default imports)。