In the introductory tutorial you learned how to create simple tasks. You also learned how to add additional behavior to these tasks later on, and you learned how to create dependencies between tasks. This was all about simple tasks, but Gradle takes the concept of tasks further. Gradle supports 增强任务, which are tasks that have their own properties and methods. This is really different from what you are used to with Ant targets. Such enhanced tasks are either provided by you or built into Gradle.

Task outcomes

当Gradle执行任务时,它可以在控制台UI中和通过Tooling API将任务标记为不同的结果. 这些标签基于任务是否具有要执行的动作,是否应执行这些动作,是否确实执行了这些动作以及这些动作是否进行了更改.

(no label) or EXECUTED

任务执行了其动作.

  • Task有动作,Gradle已确定应将其作为构建的一部分执行.

  • 任务没有动作,并且有一些依赖关系,并且任何依赖关系都将执行. 另请参见生命周期任务 .

UP-TO-DATE

任务的输出未更改.

  • 任务具有输出和输入,并且它们没有改变. 请参阅增量构建 .

  • 任务有动作,但是任务告诉Gradle它没有更改其输出.

  • Task没有任何动作,并且有一些依赖关系,但是所有依赖关系都是最新的,已跳过或来自缓存. 另请参见生命周期任务 .

  • 任务没有动作,也没有依赖关系.

FROM-CACHE

任务的输出可以从先前的执行中找到.

  • 任务的输出已从构建缓存中还原. 请参阅构建缓存 .

SKIPPED

任务未执行其动作.

NO-SOURCE

任务不需要执行其动作.

Defining tasks

本章中,我们已经看到了如何使用字符串作为任务名称来定义任务. 此样式有一些变体,您可能需要在某些情况下使用.

在" 避免任务配置"一章中将更详细地描述任务配置API.

示例1.使用字符串作为任务名称来定义任务
build.gradle
task('hello') {
    doLast {
        println "hello"
    }
}

task('copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}
build.gradle.kts
tasks.register("hello") {
    doLast {
        println("hello")
    }
}

tasks.register<Copy>("copy") {
    from(file("srcDir"))
    into(buildDir)
}

有一种定义任务的替代语法,您可能更喜欢使用:

示例2.使用tasks容器定义任务
build.gradle
tasks.create('hello') {
    doLast {
        println "hello"
    }
}

tasks.create('copy', Copy) {
    from(file('srcDir'))
    into(buildDir)
}
build.gradle.kts
tasks.register("hello") {
    doLast {
        println("hello")
    }
}

tasks {
    register<Copy>("copy") {
        from(file("srcDir"))
        into(buildDir)
    }
}

在这里,我们将任务添加到tasks集合. 看一下TaskContainer ,了解register()方法的更多变化.

最后,Groovy和Kotlin DSL有特定于语言的语法:

例子3.使用DSL特定语法定义任务
build.gradle
// Using Groovy dynamic keywords

task(hello) {
    doLast {
        println "hello"
    }
}

task(copy, type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}
build.gradle.kts
// Using Kotlin delegated properties

val hello by tasks.registering {
    doLast {
        println("hello")
    }
}

val copy by tasks.registering(Copy::class) {
    from(file("srcDir"))
    into(buildDir)
}

请注意,如果您需要创建的任务以供进一步参考,则Kotlin 委托属性语法特别有用.

Locating tasks

您通常需要找到在构建文件中定义的任务,例如,对其进行配置或将其用于依赖项. 有很多方法可以做到这一点. 首先,就像定义任务一样,Groovy和Kotlin DSL具有特定于语言的语法:

例子4.使用DSL特定语法访问任务
build.gradle
task hello
task copy(type: Copy)

// Access tasks using Groovy dynamic properties on Project

println hello.name
println project.hello.name

println copy.destinationDir
println project.copy.destinationDir
build.gradle.kts
task("hello")
task<Copy>("copy")

// Access tasks using Kotlin delegated properties

val hello by tasks.getting
println(hello.name)

val copy by tasks.getting(Copy::class)
println(copy.destinationDir)

任务也可以通过tasks集合获得.

例子5.通过任务集合访问任务
build.gradle
task hello
task copy(type: Copy)

println tasks.hello.name
println tasks.named('hello').get().name

println tasks.copy.destinationDir
println tasks.named('copy').get().destinationDir
build.gradle.kts
tasks.register("hello")
tasks.register<Copy>("copy")

println(tasks["hello"].name)
println(tasks.named("hello").get().name)

println(tasks.getByName<Copy>("copy").destinationDir)
println(tasks.named<Copy>("copy").get().destinationDir)

您可以使用tasks.getByPath()方法使用任务路径从任何项目访问任务. 您可以使用任务名称,相对路径或绝对路径来调用getByPath()方法.

例子6.通过路径访问任务
build.gradle
project(':projectA') {
    task hello
}

task hello

println tasks.getByPath('hello').path
println tasks.getByPath(':hello').path
println tasks.getByPath('projectA:hello').path
println tasks.getByPath(':projectA:hello').path
build.gradle.kts
project(":projectA") {
    tasks.register("hello")
}

tasks.register("hello")

println(tasks.getByPath("hello").path)
println(tasks.getByPath(":hello").path)
println(tasks.getByPath("projectA:hello").path)
println(tasks.getByPath(":projectA:hello").path)
gradle -q hello输出
> gradle -q hello
:hello
:hello
:projectA:hello
:projectA:hello

请查看TaskContainer,以获取更多用于定位任务的选项.

Configuring tasks

例如,让我们看一下Gradle提供的" Copy任务. 要为构建创建Copy任务,可以在构建脚本中声明:

例子7.创建复制任务
build.gradle
task myCopy(type: Copy)
build.gradle.kts
tasks.register<Copy>("myCopy")

这将创建一个没有默认行为的复制任务. 可以使用其API来配置任务(请参见Copy ). 以下示例显示了实现同一配置的几种不同方法.

为了清楚myCopy ,请意识到此任务的名称为" myCopy ",但类型为 " Copy ". 您可以具有多个类型相同但名称不同的任务. 您会发现这为您提供了强大的能力来跨特定类型的所有任务实施跨领域关注点.

示例8.使用API​​配置任务
build.gradle
Copy myCopy = tasks.getByName("myCopy")
myCopy.from 'resources'
myCopy.into 'target'
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')
build.gradle.kts
val myCopy = tasks.named<Copy>("myCopy")
myCopy {
    from("resources")
    into("target")
    include("**/*.txt", "**/*.xml", "**/*.properties")
}

这类似于我们在Java中配置对象的方式. 您必须每次在配置语句中重复上下文( myCopy ). 这是多余的,阅读起来也不是很好.

还有另一种配置任务的方法. 它还保留了上下文,并且可以说是最易读的. 通常是我们的最爱.

示例9.使用DSL特定语法配置任务
build.gradle
// Configure task using Groovy dynamic task configuration block
myCopy {
   from 'resources'
   into 'target'
}
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')
build.gradle.kts
// Configure task using Kotlin delegated properties and a lambda
val myCopy by tasks.existing(Copy::class) {
    from("resources")
    into("target")
}
myCopy { include("**/*.txt", "**/*.xml", "**/*.properties") }

这适用于任何任务. 任务访问只是tasks.named() (Kotlin)或tasks.getByName() (Groovy)方法的快捷方式. 重要的是要注意,此处使用的块用于配置任务,并且在任务执行时不进行评估.

看一下TaskContainer ,了解更多配置任务的选项.

定义任务时,也可以使用配置块.

例子10.用配置块定义一个任务
build.gradle
task copy(type: Copy) {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}
build.gradle.kts
tasks.register<Copy>("copy") {
   from("resources")
   into("target")
   include("**/*.txt", "**/*.xml", "**/*.properties")
}
💡
不要忘记构建阶段

任务具有配置和动作. 使用doLast ,您只是在使用快捷方式来定义动作. 任务的配置部分中定义的代码将在构建的配置阶段执行,无论目标任务是什么. 有关构建生命周期的更多详细信息,请参见构建生命周期.

Passing arguments to a task constructor

与在创建后配置Task的可变属性相反,您可以将参数值传递给Task类的构造函数. 为了将值传递给Task构造函数,必须使用@javax.inject.Inject注释相关的构造函数.

例子11.具有@Inject构造函数的任务类
build.gradle
class CustomTask extends DefaultTask {
    final String message
    final int number

    @Inject
    CustomTask(String message, int number) {
        this.message = message
        this.number = number
    }
}
build.gradle.kts
open class CustomTask @Inject constructor(
    private val message: String,
    private val number: Int
) : DefaultTask()

然后,您可以创建一个任务,并在参数列表的末尾传递构造函数参数.

例子12.使用TaskContainer创建一个带有构造函数参数的任务
build.gradle
tasks.create('myTask', CustomTask, 'hello', 42)
build.gradle.kts
tasks.register<CustomTask>("myTask", "hello", 42)

您还可以使用Project API使用constructorArgs Map参数创建任务:

例子13.使用Map创建带有构造函数参数的任务
build.gradle
task myTask(type: CustomTask, constructorArgs: ['hello', 42])
build.gradle.kts
task("myTask", "type" to CustomTask::class.java, "constructorArgs" to listOf("hello", 42))
首选使用TaskContainer使用构造函数参数创建任务

建议使用" 避免任务配置" API来缩短配置时间.

在所有情况下,作为构造函数参数传递的值都必须为非null. 如果尝试传递null值,则Gradle将抛出NullPointerException指示哪个运行时值为null .

Adding dependencies to a task

您可以通过多种方式定义任务的依赖关系. 在" 任务依赖项"中 ,介绍了使用任务名称定义依赖项. 任务名称可以引用与该任务在同一项目中的任务,也可以引用其他项目中的任务. 要引用另一个项目中的任务,请在任务名称前添加其所属项目的路径. 以下是将projectA:taskX的依赖projectA:taskXprojectB:taskY

例子14.从另一个项目添加对任务的依赖
build.gradle
project('projectA') {
    task taskX {
        dependsOn ':projectB:taskY'
        doLast {
            println 'taskX'
        }
    }
}

project('projectB') {
    task taskY {
        doLast {
            println 'taskY'
        }
    }
}
build.gradle.kts
project("projectA") {
    tasks.register("taskX") {
        dependsOn(":projectB:taskY")
        doLast {
            println("taskX")
        }
    }
}

project("projectB") {
    tasks.register("taskY") {
        doLast {
            println("taskY")
        }
    }
}
gradle -q taskX输出
> gradle -q taskX
taskY
taskX

您可以使用Task对象定义依赖项,而不是使用任务名称,如以下示例所示:

例子15.使用任务对象添加依赖项
build.gradle
task taskX {
    doLast {
        println 'taskX'
    }
}

task taskY {
    doLast {
        println 'taskY'
    }
}

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

val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}

taskX {
    dependsOn(taskY)
}
gradle -q taskX输出
> gradle -q taskX
taskY
taskX

对于更高级的用途,您可以使用惰性块定义任务依赖项. 求值时,会将正在​​计算依赖关系的任务传递给该块. 惰性块应返回单个TaskTask对象的集合,然后将其视为任务的依赖项. 以下示例将taskX的依赖项添加到名称以lib开头的项目中的所有任务:

例子16.使用惰性块添加依赖项
build.gradle
task taskX {
    doLast {
        println 'taskX'
    }
}

// Using a Groovy Closure
taskX.dependsOn {
    tasks.findAll { task -> task.name.startsWith('lib') }
}

task lib1 {
    doLast {
        println 'lib1'
    }
}

task lib2 {
    doLast {
        println 'lib2'
    }
}

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

// Using a Gradle Provider
taskX {
    dependsOn(provider {
        tasks.filter { task -> task.name.startsWith("lib") }
    })
}

tasks.register("lib1") {
    doLast {
        println("lib1")
    }
}

tasks.register("lib2") {
    doLast {
        println("lib2")
    }
}

tasks.register("notALib") {
    doLast {
        println("notALib")
    }
}
gradle -q taskX输出
> gradle -q taskX
lib1
lib2
taskX

有关任务依赖关系的更多信息,请参见任务 API.

Ordering tasks

在某些情况下,控制两个任务的执行顺序很有用,而不必在这些任务之间引入明确的依赖关系. 任务排序和任务相关性之间的主要区别在于,排序规则不影响将执行哪些任务,仅影响将执行它们的顺序.

任务排序在许多情况下很有用:

  • 强制执行任务的顺序排序:例如," build"在" clean"之前永远不会运行.

  • 在构建的早期进行构建验证:例如,在开始发布构建工作之前,请先验证我是否具有正确的凭据.

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

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

有两种可用的排序规则:" 必须在 " 之后运行和" 应在 " 之后运行 .

当您使用"必须在之后运行"排序规则时,您指定taskB必须始终在taskA之后taskA ,无论何时taskAtaskB都将运行. 这表示为taskB.mustRunAfter(taskA) . "应在之后运行"排序规则是相似的,但不太严格,因为它将在两种情况下被忽略. 首先,如果使用该规则会引入一个订购周期. 其次,当使用并行执行并且任务的所有依赖关系都已满足("应在之后运行"任务除外)时,则将运行该任务,而不管其"应在之后运行"依赖关系是否已运行. 您应该使用"应该在之后运行",在该顺序很有帮助但并非严格要求的情况下.

有了这些规则,仍然可以在没有taskB情况下执行taskA ,反之亦然.

例子17.添加一个"必须在之后运行"任务排序
build.gradle
task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}
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) }
gradle -q taskY taskX输出
> gradle -q taskY taskX
taskX
taskY
例子18.添加一个"应该在之后运行"任务排序
build.gradle
task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}
taskY.shouldRunAfter taskX
build.gradle.kts
val taskX by tasks.registering {
    doLast {
        println("taskX")
    }
}
val taskY by tasks.registering {
    doLast {
        println("taskY")
    }
}
taskY { shouldRunAfter(taskX) }
gradle -q taskY taskX输出
> gradle -q taskY taskX
taskX
taskY

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

例子19.任务排序并不意味着任务执行
gradle -q taskY输出
> gradle -q taskY
taskY

要指定两个任务之间的"必须在之后运行"或"应在之后运行"顺序,请使用Task.mustRunAfter(java.lang.Object ...)Task.shouldRunAfter(java.lang.Object ...)方法. 这些方法接受Task.dependsOn(java.lang.Object ...)接受的任务实例,任务名称或任何其他输入.

请注意," B.mustRunAfter(A) "或" B.shouldRunAfter(A) "并不意味着任务之间的执行依赖关系:

  • 可以独立执行任务AB 只有两个任务都计划执行时,排序规则才有效.

  • 使用--continue运行时,如果A失败,则B可能会执行.

如前所述,如果引入了订购周期,则"应在之后运行"订购规则将被忽略:

例子20.如果引入了一个订购周期,则"应在之后运行"任务订购被忽略
build.gradle
task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}
task taskZ {
    doLast {
        println 'taskZ'
    }
}
taskX.dependsOn taskY
taskY.dependsOn taskZ
taskZ.shouldRunAfter taskX
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) }
gradle -q taskX输出
> gradle -q taskX
taskZ
taskY
taskX

Adding a description to a task

You can add a description to your task. This description is displayed when executing gradle tasks.

例子21.给任务添加描述
build.gradle
task copy(type: Copy) {
   description 'Copies the resource directory to the target directory.'
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}
build.gradle.kts
tasks.register<Copy>("copy") {
   description = "Copies the resource directory to the target directory."
   from("resources")
   into("target")
   include("**/*.txt", "**/*.xml", "**/*.properties")
}

Skipping tasks

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

Using a predicate

您可以使用onlyIf()方法将谓词附加到任务. 仅当谓词评估为true时,才执行任务的动作. 您将谓词实现为闭包. 闭包作为参数传递给任务,如果任务应执行,则应返回true;如果应跳过任务,则应返回false. 在即将执行任务之前就对谓词进行评估.

例子22.使用谓词跳过任务
build.gradle
task hello {
    doLast {
        println 'hello world'
    }
}

hello.onlyIf { !project.hasProperty('skipHello') }
build.gradle.kts
val hello by tasks.registering {
    doLast {
        println("hello world")
    }
}

hello {
    onlyIf { !project.hasProperty("skipHello") }
}
Output of gradle hello -PskipHello
> gradle hello -PskipHello
> Task :hello SKIPPED

BUILD SUCCESSFUL in 0s

Using StopExecutionException

如果不能用谓词来表示跳过任务的逻辑,则可以使用StopExecutionException . 如果某个动作引发了此异常,则将跳过该动作的进一步执行以及该任务的任何后续动作的执行. 构建继续执行下一个任务.

例子23.使用StopExecutionException跳过任务
build.gradle
task compile {
    doLast {
        println 'We are doing the compile.'
    }
}

compile.doFirst {
    // Here you would put arbitrary conditions in real life.
    // But this is used in an integration test so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task myTask {
    dependsOn('compile')
    doLast {
        println 'I am not affected'
    }
}
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.
        // But this is used in an integration test so we want defined behavior.
        if (true) {
            throw StopExecutionException()
        }
    }
}
tasks.register("myTask") {
    dependsOn(compile)
    doLast {
        println("I am not affected")
    }
}
gradle -q myTask输出
> gradle -q myTask
I am not affected

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

Enabling and disabling tasks

每个任务都有一个enabled标志,默认为true . 将其设置为false阻止执行任务的任何动作. 禁用的任务将标记为"跳过".

例子24.启用和禁用任务
build.gradle
task disableMe {
    doLast {
        println 'This should not be printed if the task is disabled.'
    }
}
disableMe.enabled = false
build.gradle.kts
val disableMe by tasks.registering {
    doLast {
        println("This should not be printed if the task is disabled.")
    }
}
disableMe {
    enabled = false
}
gradle disableMe输出
> gradle disableMe
> Task :disableMe SKIPPED

BUILD SUCCESSFUL in 0s

Task timeouts

每个任务都有一个timeout属性,可以用来限制其执行时间. 当任务达到超时时,其任务执行线程将被中断. 该任务将被标记为失败. 终结器任务仍将运行. 如果使用--continue ,其他任务可以在其后继续运行. 不响应中断的任务无法超时. Gradle的所有内置任务均会及时响应超时.

例子25.指定任务超时
build.gradle
task hangingTask() {
    doLast {
        Thread.sleep(100000)
    }
    timeout = Duration.ofMillis(500)
}
build.gradle.kts
import java.time.Duration

tasks {
    register("hangingTask") {
        doLast {
            Thread.sleep(100000)
        }
        timeout.set(Duration.ofMillis(500))
    }
}

Up-to-date checks (AKA Incremental Build)

任何构建工具的重要组成部分是避免执行已经完成的工作的能力. 考虑编译过程. 编译完源文件后,除非更改了一些会影响输出的内容(例如修改源文件或删除输出文件),否则就无需重新编译它们. 而且编译可能要花费大量时间,因此在不需要时跳过该步骤可以节省大量时间.

Gradle通过其称为增量构建的功能来开箱即用地支持此行为. 您几乎可以肯定已经在实际中看到了它:运行构建时,它几乎每次在任务名称旁边出现UP-TO-DATE文本时都会激活. 任务结果在任务结果中进行了描述.

增量构建如何工作? 在自己的任务中需要使用什么? 让我们来看看.

Task inputs and outputs

在最常见的情况下,任务需要一些输入并产生一些输出. 如果使用前面的编译示例,则可以看到源文件是输入,对于Java,生成的类文件是输出. 其他输入可能包括诸如是否应包含调试信息之类的内容.

taskInputsOutputs
图1.示例任务输入和输出

如上图所示,输入的一个重要特征是它会影响一个或多个输出. 根据源文件的内容以及要在其上运行代码的Java运行时的最低版本,会生成不同的字节码. 这使它们成为任务输入. 但是,编译是否具有500MB或600MB的最大可用内存(由memoryMaximumSize属性确定)对生成什么字节码没有影响. 用Gradle术语来说, memoryMaximumSize只是一个内部任务属性.

作为增量构建的一部分,Gradle会测试自上次构建以来是否已更改任何任务输入或输出. 如果还没有,Gradle可以考虑该任务是最新的,因此跳过执行其动作. 另请注意,除非任务通常至少也有一个输入,否则除非一个任务至少有一个任务输出,否则增量构建将不起作用.

对于构建作者而言,这很简单:您需要告诉Gradle哪些任务属性是输入,哪些是输出. 如果任务属性影响输出,请确保将其注册为输入,否则将认为该任务是最新的. 相反,如果属性不会影响输出,则不要将其注册为输入,否则任务可能会在不需要时执行. 也要注意那些不确定的任务,这些任务可能为完全相同的输入生成不同的输出:不应为增量构建配置这些任务,因为最新的检查将不起作用.

现在让我们看一下如何将任务属性注册为输入和输出.

Custom task types

如果您要以类的形式实现自定义任务,则只需两步即可使其与增量构建一起使用:

  1. 为每个任务输入和输出创建类型化的属性(通过getter方法)

  2. 向每个属性添加适当的注释

注释必须放在吸气剂或Groovy属性上. 放置在setter上或没有相应带注释的getter的Java字段上的注释将被忽略.

Gradle支持三种主要的输入和输出类别:

例如,假设您有一个任务来处理各种类型的模板,例如FreeMarker,Velocity,Moustache等.它需要模板源文件,并将它们与一些模型数据结合起来以生成模板文件的填充版本.

该任务将具有三个输入和一个输出:

  • 模板源文件

  • 模型数据

  • 模板引擎

  • 写入输出文件的位置

在编写自定义任务类时,很容易通过注释将属性注册为输入或输出. 为了演示,这是一个基本任务实现,其中包含一些合适的输入和输出以及它们的注释:

例子26.定制任务类
buildSrc/src/main/java/org/example/ProcessTemplates.java
package org.example;

import java.io.File;
import java.util.HashMap;
import org.gradle.api.*;
import org.gradle.api.file.*;
import org.gradle.api.tasks.*;

public class ProcessTemplates extends DefaultTask {
    private TemplateEngineType templateEngine;
    private FileCollection sourceFiles;
    private TemplateData templateData;
    private File outputDir;

    @Input
    public TemplateEngineType getTemplateEngine() {
        return this.templateEngine;
    }

    @InputFiles
    public FileCollection getSourceFiles() {
        return this.sourceFiles;
    }

    @Nested
    public TemplateData getTemplateData() {
        return this.templateData;
    }

    @OutputDirectory
    public File getOutputDir() { return this.outputDir; }

    // + setter methods for the above - assume we’ve defined them

    @TaskAction
    public void processTemplates() {
        // ...
    }
}
buildSrc/src/main/java/org/example/TemplateData.java
package org.example;

import java.util.HashMap;
import java.util.Map;
import org.gradle.api.tasks.Input;

public class TemplateData {
    private String name;
    private Map<String, String> variables;

    public TemplateData(String name, Map<String, String> variables) {
        this.name = name;
        this.variables = new HashMap<>(variables);
    }

    @Input
    public String getName() { return this.name; }

    @Input
    public Map<String, String> getVariables() {
        return this.variables;
    }
}
gradle processTemplates输出
> gradle processTemplates
> Task :processTemplates


BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
gradle processTemplates输出(再次运行)
> gradle processTemplates
> Task :processTemplates UP-TO-DATE

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

在此示例中有很多要讨论的内容,因此让我们依次研究每个输入和输出属性:

  • templateEngine

    表示处理源模板(例如FreeMarker,Velocity等)时要使用的引擎.您可以将其实现为字符串,但是在这种情况下,我们选择了自定义枚举,因为它提供了更多的类型信息和安全性. 由于枚举会自动实现Serializable ,因此我们可以将其视为一个简单值并使用@Input批注,就像使用String属性一样.

  • sourceFiles

    任务将要处理的源模板. 单个文件和文件集合需要它们自己的特殊注释. 在这种情况下,我们正在处理输入文件的集合,因此我们使用@InputFiles批注. 稍后,您会在表格中看到更多面向文件的注释.

  • templateData

    对于此示例,我们使用一个自定义类来表示模型数据. 但是,它没有实现Serializable ,因此我们不能使用@Input批注. 这不是问题,因为TemplateData的属性(具有可序列化类型参数的字符串和哈希图)是可序列化的,并且可以使用@Input进行注释. 我们在templateData上使用@Nested ,以使Gradle知道这是具有嵌套输入属性的值.

  • outputDir

    生成的文件所在的目录. 与输入文件一样,输出文件和目录也有一些注释. 表示单个目录的属性需要@OutputDirectory . 您很快就会了解其他.

这些带注释的属性意味着,自从Gradle上次执行任务以来,如果源文件,模板引擎,模型数据或生成的文件均未更改,则Gradle将跳过任务. 这通常会节省大量时间. 您可以稍后了解Gradle如何检测更改 .

这个示例特别有趣,因为它可以处理源文件的集合. 如果仅更改一个源文件,会发生什么? 该任务会再次处理所有源文件还是仅处理已修改的源文件? 这取决于任务的实现. 如果是后者,则任务本身就是增量的,但这与我们在此讨论的功能不同. Gradle通过其增量任务输入功能确实可以帮助任务实施者.

现在您已经在实践中看到了一些输入和输出注释,下面让我们看一下所有可用的注释以及何时使用它们. 下表列出了可用的注释以及可以与每个注释一起使用的相应属性类型.

表1.增量构建属性类型注释
Annotation 预期物业类型 Description

Any Serializable type

一个简单的输入值

File*

单个输入文件(不是目录)

File*

单个输入目录(不是文件)

Iterable<File>*

输入文件和目录的迭代

Iterable<File>*

代表Java类路径的输入文件和目录的可迭代项. 这使任务可以忽略对该属性的不相关更改,例如相同文件的名称不同. 它类似于对属性@PathSensitive(RELATIVE)进行注释,但是它将忽略直接添加到类路径中的JAR文件的名称,并且会将文件顺序的更改视为类路径中的更改. Gradle将检查类路径上jar文件的内容,并忽略不影响类路径语义的更改(例如文件日期和输入顺序). 另请参见使用类路径注释 .

注意: @Classpath批注是在Gradle 3.2中引入的. 为了与Gradle的早期版本保持兼容,类路径属性也应使用@InputFiles进行注释.

Iterable<File>*

代表Java编译类路径的输入文件和目录的可迭代项. 这使任务可以忽略不影响classpath中类的API的不相关更改. 另请参见使用类路径注释 .

对类路径的以下几种更改将被忽略:

  • 更改jar或顶级目录的路径.

  • 更改时间戳和Jars中条目的顺序.

  • 对资源和Jar清单的更改,包括添加或删除资源.

  • 更改私有类元素,例如私有字段,方法和内部类.

  • 对代码的更改,例如方法体,静态初始化器和字段初始化器(常量除外).

  • 调试信息的更改,例如,对注释的更改会影响类调试信息中的行号.

  • 对目录的更改,包括Jars中的目录条目.

@CompileClasspath注释是在Gradle 3.4中引入的. 为了与Gradle 3.3和3.2保持兼容,编译类路径属性也应使用@Classpath注释. 为了与3.2之前的Gradle版本兼容,该属性也应使用@InputFiles注释.

File*

单个输出文件(非目录)

File*

单个输出目录(不是文件)

Map<String, File> **或Iterable<File> *

输出文件的可迭代或映射. 使用文件树将关闭该任务的缓存 .

Map<String, File> **或Iterable<File> *

可迭代的输出目录. 使用文件树将关闭该任务的缓存 .

File or Iterable<File>*

指定此任务删除的一个或多个文件. 请注意,任务可以定义输入/输出或可销毁对象,但不能同时定义两者.

File or Iterable<File>*

指定一个或多个表示任务本地状态的文件. 从缓存中加载任务时,将删除这些文件.

任何自定义类型

一种自定义类型,可能无法实现Serializable但是至少具有一个用此表中的注释之一标记的字段或属性. 它甚至可能是另一个@Nested .

随便哪种

指示该属性既不是输入也不是输出. 它只是以某种方式影响任务的控制台输出,例如增加或减少任务的详细程度.

随便哪种

指示该属性在内部使用,但既不是输入也不是输出.

随便哪种

指示该属性已被另一个替换,应作为输入或输出忽略.

File*

@InputFiles@InputDirectory一起使用,以指示Gradle如果相应的文件或目录为空以及使用此注释声明的所有其他输入文件为空,则跳过该任务. 由于使用该批注声明为空的所有输入文件而被跳过的任务将导致明显的"无源"结果. 例如,在控制台输出中将发出NO-SOURCE .

Implies @Incremental.

Provider<FileSystemLocation> or FileCollection

@InputFiles@InputDirectory一起使用,以指示Gradle跟踪对带注释的文件属性的更改,因此可以通过@ InputChanges.getFileChanges()查询更改. 增量任务所需.

随便哪种

可选 API文档中列出的任何属性类型注释一起使用. 该批注禁用对相应属性的验证检查. 有关更多详细信息,请参见验证部分 .

File*

与任何输入文件属性一起使用,以告诉Gradle仅将文件路径的给定部分视为重要. 例如,如果使用@PathSensitive(PathSensitivity.NAME_ONLY)注释属性,则在不更改文件内容的情况下移动文件不会使任务过时.

*

实际上, File可以是Project.file(java.lang.Object)接受的任何类型, Iterable<File>可以是Project.files(java.lang.Object…)接受的任何类型. 这包括Callable实例(例如闭包),允许对属性值进行延迟评估. 请注意,类型FileCollectionFileTreeIterable<File> .

**

与上述类似, File可以是Project.file(java.lang.Object)接受的任何类型. Map本身可以包装在Callable ,例如闭包.

注释从所有父类型(包括已实现的接口)继承. 属性类型注释会覆盖父类型中声明的任何其他属性类型注释. 这样,可以将@InputFile属性转换为子任务类型中的@InputDirectory属性.

在类型中声明的属性的注释会覆盖超类和任何已实现接口中声明的类似注释. 超类注释优先于已实现接口中声明的注释.

表格中的ConsoleInternal注释是特殊情况,因为它们未声明任务输入或任务输出. 那为什么要使用它们呢? 这样一来,您就可以利用Java Gradle插件开发插件来帮助您开发和发布自己的插件. 该插件检查您的自定义任务类的任何属性是否缺少增量构建注释. 这样可以防止您在开发过程中忘记添加适当的注释.

Using the classpath annotations

除了@InputFiles ,对于与JVM相关的任务,Gradle还了解类路径输入的概念. 当Gradle寻找更改时,对运行时和编译类路径的处理会有所不同.

与用@ InputFiles注释的输入属性相反,对于类路径属性,文件集合中条目的顺序很重要. 另一方面,类路径本身上的目录和jar文件的名称和路径将被忽略. 类路径上jar文件中的时间戳以及类文件和资源的顺序也将被忽略,因此重新创建具有不同文件日期的jar文件不会使任务过期.

运行时类路径用@ Classpath标记,它们通过类路径规范化提供进一步的自定义.

@ CompileClasspath注释的输入属性被视为Java编译类路径. 除上述常规类路径规则外,编译类路径还会忽略对除类文件以外的所有内容的更改. Gradle使用Java避免编译中描述的相同类分析来进一步过滤不影响类ABI的更改. 这意味着仅涉及类实现的更改不会使任务过时.

Nested inputs

在分析@ Nested任务属性以获取已声明的输入和输出子属性时,Gradle使用实际值的类型. 因此,它可以发现运行时子类型声明的所有子属性.

当添加@ NestedProvider ,所述的值Provider将被视为一个嵌套输入.

@ Nested添加到可迭代对象时,每个元素都被视为单独的嵌套输入. 在Iterable中为每个嵌套输入分配一个名称,默认情况下是美元符号,后跟Iterable中的索引,例如$2 . 如果iterable的元素实现Named ,则将该名称用作属性名称. 如果不是所有元素都实现Named ,则iterable中元素的顺序对于可靠的最新检查和缓存至关重要. 不允许使用多个具有相同名称的元素.

@ Nested添加到地图时,然后使用键作为名称为每个值添加一个嵌套输入.

嵌套输入的类型和类路径也将被跟踪. 这确保了对嵌套输入的实现的更改会导致构建过时. 这样,也可以添加用户提供的代码作为输入,例如通过使用@ Nested注释@ Action属性. 请注意,应通过操作上的带注释的属性或通过在任务中手动注册这些操作来跟踪对此类操作的任何输入.

使用嵌套输入可以为任务提供更丰富的建模和可扩展性,例如Test.getJvmArgumentProviders()所示.

这使我们可以对JaCoCo Java代理进行建模,从而声明必要的JVM参数并将输入和输出提供给Gradle:

JacocoAgent.java
class JacocoAgent implements CommandLineArgumentProvider {
    private final JacocoTaskExtension jacoco;

    public JacocoAgent(JacocoTaskExtension jacoco) {
        this.jacoco = jacoco;
    }

    @Nested
    @Optional
    public JacocoTaskExtension getJacoco() {
        return jacoco.isEnabled() ? jacoco : null;
    }

    @Override
    public Iterable<String> asArguments() {
        return jacoco.isEnabled() ? ImmutableList.of(jacoco.getAsJvmArg()) : Collections.<String>emptyList();
    }
}

test.getJvmArgumentProviders().add(new JacocoAgent(extension));

为此, JacocoTaskExtension需要具有正确的输入和输出注释.

该方法适用于Test JVM参数,因为Test.getJvmArgumentProviders()是使用@ Nested注释的Iterable .

还有其他类型的嵌套输入可用的任务类型:

同样,这种建模可用于自定义任务.

Runtime validation

在执行构建时,Gradle检查任务类型是否用适当的注释声明. 它尝试识别问题,例如在不兼容的类型或setter等上使用注释.未标记有输入/输出注释的任何getter也会被标记. 然后,在执行任务时,这些问题就会变成弃用警告.

带有未声明输入和输出的任务的示例输出
> gradle processTemplatesRuntime
> Task :processTemplatesRuntime
Property 'outputDir' is not annotated with an input or output annotation. This behaviour has been deprecated and is scheduled to be removed in Gradle 7.0.
Property 'sourceFiles' is not annotated with an input or output annotation. This behaviour has been deprecated and is scheduled to be removed in Gradle 7.0.
Property 'templateData' is not annotated with an input or output annotation. This behaviour has been deprecated and is scheduled to be removed in Gradle 7.0.
Property 'templateEngine' is not annotated with an input or output annotation. This behaviour has been deprecated and is scheduled to be removed in Gradle 7.0.


BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Runtime API

自定义任务类是将您自己的构建逻辑带入增量构建领域的一种简便方法,但是您并不总是有这种选择. 因此,Gradle还提供了可用于任何任务的替代API,我们接下来将介绍.

如果您无权访问自定义任务类的源,则无法添加上一节中介绍的任何注释. 幸运的是,Gradle为此类场景提供了运行时API. 它也可以用于临时任务,正如您将在下面看到的那样.

Using it for ad-hoc tasks

This runtime API is provided through a couple of aptly named properties that are available on every Gradle task:

这些对象具有允许您指定构成任务输入和输出的文件,目录和值的方法. 实际上,运行时API与注释几乎具有同等功能. 它缺少的只是@ Nested的等效项.

让我们以之前的模板处理示例为例,看一下它是使用运行时API的即席任务的外观:

例子27.临时任务
build.gradle
task processTemplatesAdHoc {
    inputs.property("engine", TemplateEngineType.FREEMARKER)
    inputs.files(fileTree("src/templates"))
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property("templateData.name", "docs")
    inputs.property("templateData.variables", [year: 2013])
    outputs.dir("$buildDir/genOutput2")
        .withPropertyName("outputDir")

    doLast {
        // Process the templates here
    }
}
build.gradle.kts
tasks.register("processTemplatesAdHoc") {
    inputs.property("engine", TemplateEngineType.FREEMARKER)
    inputs.files(fileTree("src/templates"))
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property("templateData.name", "docs")
    inputs.property("templateData.variables", mapOf("year" to "2013"))
    outputs.dir("$buildDir/genOutput2")
        .withPropertyName("outputDir")

    doLast {
        // Process the templates here
    }
}
gradle processTemplatesAdHoc输出
> gradle processTemplatesAdHoc
> Task :processTemplatesAdHoc

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

As before, there’s much to talk about. To begin with, you should really write a custom task class for this as it’s a non-trivial implementation that has several configuration options. In this case, there are no task properties to store the root source folder, the location of the output directory or any of the other settings. That’s deliberate to highlight the fact that the runtime API doesn’t require the task to have any state. In terms of incremental build, the above ad-hoc task will behave the same as the custom task class.

所有输入和输出定义都是通过inputsoutputs上的方法完成的,例如property()files()dir() . Gradle对参数值执行最新检查,以确定任务是否需要再次运行. 每个方法都对应一个增量构建批注,例如, inputs.property()映射到@Input@Input outputs.dir()映射到@OutputDirectory .

任务删除的文件可以通过destroyables.register()指定.

例子28.宣告可销毁的临时任务
build.gradle
task removeTempDir {
    destroyables.register("$projectDir/tmpDir")
    doLast {
        delete("$projectDir/tmpDir")
    }
}
build.gradle.kts
tasks.register("removeTempDir") {
    destroyables.register("$projectDir/tmpDir")
    doLast {
        delete("$projectDir/tmpDir")
    }
}

运行时API和注释之间的显着区别是缺少直接与@Nested对应的方法. 这就是为什么该示例对模板数据使用两个property()声明,对每个TemplateData属性使用一个声明的原因. 在将运行时API与嵌套值一起使用时,应使用相同的技术. 任何给定的任务都可以声明可销毁物品或输入/输出,但不能同时声明两者.

Fine-grained configuration

运行时API方法仅允许您自己声明输入和输出. 但是,面向文件的工具会返回一个构造器-类型TaskInputFilePropertyBuilder-使您可以提供有关这些输入和输出的其他信息.

您可以在其API文档中了解该构建器提供的所有选项,但在这里我们将向您展示一个简单的示例,让您大致了解可以做什么.

假设如果没有源文件,我们就不想运行processTemplates任务,而不管它是否是干净的构建. 毕竟,如果没有源文件,则无需执行任何任务. 构建器允许我们这样配置:

例子29.通过运行时API使用skipWhenEmpty()
build.gradle
task processTemplatesAdHocSkipWhenEmpty {
    // ...

    inputs.files(fileTree("src/templates") {
            include "**/*.fm"
        })
        .skipWhenEmpty()
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)

    // ...
}
build.gradle.kts
tasks.register("processTemplatesAdHocSkipWhenEmpty") {
    // ...

    inputs.files(fileTree("src/templates") {
            include("**/*.fm")
        })
        .skipWhenEmpty()
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)

    // ...
}
gradle clean processTemplatesAdHocSkipWhenEmpty输出
> gradle clean processTemplatesAdHocSkipWhenEmpty
> Task :processTemplatesAdHocSkipWhenEmpty NO-SOURCE


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

TaskInputs.files()方法返回一个具有skipWhenEmpty()方法的生成器. 调用此方法等效于使用@SkipWhenEmpty注释属性.

现在您已经看到了注释和运行时API,您可能想知道应该使用哪个API. 我们的建议是尽可能使用注释,有时值得创建自定义任务类,以便您可以使用它们. 运行时API更适用于无法使用注释的情况.

Using it for custom task types

另一类示例涉及为自定义任务类的实例注册其他输入和输出. 例如,假设ProcessTemplates任务还需要读取src/headers/headers.txt (例如,因为它是从来源之一中包含的). 您希望Gradle知道此输入文件,以便只要此文件的内容更改,它就可以重新执行任务. 使用运行时API,您可以做到这一点:

例子30.使用运行时API和自定义任务类型
build.gradle
task processTemplatesWithExtraInputs(type: ProcessTemplates) {
    // ...

    inputs.file("src/headers/headers.txt")
        .withPropertyName("headers")
        .withPathSensitivity(PathSensitivity.NONE)
}
build.gradle.kts
tasks.register<ProcessTemplates>("processTemplatesWithExtraInputs") {
    // ...

    inputs.file("src/headers/headers.txt")
        .withPropertyName("headers")
        .withPathSensitivity(PathSensitivity.NONE)
}

像这样使用运行时API有点像使用doLast()doFirst()将额外的动作附加到任务上,除了在这种情况下,我们将附加关于输入和输出的信息.

如果任务类型已经在使用增量构建批注,则使用相同的属性名称注册输入或输出将导致错误.

Important beneficial side effects

一旦声明了任务的正式输入和输出,Gradle就可以推断出有关这些属性的信息. 例如,如果将一个任务的输入设置为另一个任务的输出,则意味着第一个任务取决于第二个任务,对吗? Gradle知道这一点并可以采取行动.

接下来,我们将介绍此功能,以及Gradle了解输入和输出的其他一些功能.

Inferred task dependencies

考虑一个归档任务,该任务打包了processTemplates任务的输出. 构建作者将看到,存档任务显然需要首先运行processTemplates ,因此可以添加显式的dependsOn . 但是,如果您这样定义归档任务:

Example 31. Inferred task dependency via task outputs
build.gradle
task packageFiles(type: Zip) {
    from processTemplates.outputs
}
build.gradle.kts
tasks.register<Zip>("packageFiles") {
    from(processTemplates.get().outputs)
}
gradle clean packageFiles输出
> gradle clean packageFiles
> Task :processTemplates
> Task :packageFiles


BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

Gradle将自动使packageFiles依赖于processTemplates . 之所以可以这样做,是因为它知道packageFiles的输入之一需要processTemplates任务的输出. 我们称其为推断的任务依赖性.

上面的例子也可以写成

Example 32. Inferred task dependency via a task argument
build.gradle
task packageFiles2(type: Zip) {
    from processTemplates
}
build.gradle.kts
tasks.register<Zip>("packageFiles2") {
    from(processTemplates)
}
gradle clean packageFiles2输出
> gradle clean packageFiles2
> Task :processTemplates
> Task :packageFiles2


BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

这是因为from()方法可以接受任务对象作为参数. 在幕后, from()使用project.files()方法包装参数,从而将任务的正式输出公开为文件集合. 换句话说,这是特例!

Input and output validation

增量构建批注为Gradle提供足够的信息,以对批注的属性执行一些基本验证. 特别是,在任务执行之前,它将对每个属性执行以下操作:

  • @InputFile验证该属性是否具有值,并且该路径对应于存在的文件(而非目录).

  • @InputDirectory -相同@InputFile ,除了路径必须对应于目录.

  • @OutputDirectory验证路径与文件不匹配,并在目录不存在的情况下创建目录.

这种验证提高了构建的健壮性,使您可以快速识别与输入和输出有关的问题.

您有时会希望禁用某些验证,特别是在输入文件可能确实不存在的情况下. 这就是Gradle提供@Optional批注的原因:您使用它来告诉Gradle特定输入是可选的,因此,如果对应的文件或目录不存在,构建也不会失败.

Continuous build

定义任务输入和输出的另一个好处是连续构建. 由于Gradle知道任务所依赖的文件,因此如果其任何输入发生更改,它可以自动再次运行任务. 通过在运行Gradle时激活连续构建-通过--continuous-t选项,您将使Gradle处于不断检查更改并在遇到此类更改时执行所请求任务的状态.

您可以在Continuous build中找到有关此功能的更多信息.

Task parallelism

定义任务输入和输出的最后一个好处是,当使用" --parallel"选项时,Gradle可以使用此信息来决定如何运行任务. 例如,Gradle在选择要运行的下一个任务时将检查任务的输出,并将避免并发执行写入同一输出目录的任务. 同样,Gradle将使用有关任务销毁哪些文件的信息(例如,由Destroys批注指定),并避免在运行另一个消耗或创建相同文件的任务时运行删除一组文件的任务(反之亦然). 它还可以确定创建一组文件的任务已经运行,并且消耗这些文件的任务尚未运行,并且将避免运行将两者之间的文件删除的任务. 通过以这种方式提供任务输入和输出信息,Gradle可以推断任务之间的创建/消耗/销毁关系,并可以确保任务执行不违反那些关系.

How does it work?

在第一次执行任务之前,Gradle会对输入进行指纹识别. 此指纹包含输入文件的路径以及每个文件内容的哈希. 然后Gradle执行任务. 如果任务成功完成,则Gradle将对输出进行指纹识别. 此指纹包含一组输出文件以及每个文件内容的哈希值. Gradle会在下一次执行任务时保留两个指纹.

Each time after that, before the task is executed, Gradle takes a new fingerprint of the inputs and outputs. If the new fingerprints are the same as the previous fingerprints, Gradle assumes that the outputs are up to date and skips the task. If they are not the same, Gradle executes the task. Gradle persists both fingerprints for the next time the task is executed.

如果文件的统计信息(即lastModifiedsize )未更改,则Gradle将重用上一次运行时的文件指纹. 这意味着当文件的统计信息未更改时,Gradle不会检测到更改.

Gradle还将任务代码视为任务输入的一部分. 当任务,其动作或其依赖关系在两次执行之间发生变化时,Gradle会将任务视为过时的.

Gradle了解文件属性(例如,持有Java类路径的文件属性)是否对顺序敏感. 比较此类属性的指纹时,即使文件顺序发生更改,也将导致任务过时.

Note that if a task has an output directory specified, any files added to that directory since the last time it was executed are ignored and will NOT cause the task to be out of date. This is so unrelated tasks may share an output directory without interfering with each other. If this is not the behaviour you want for some reason, consider using TaskOutputs.upToDateWhen(groovy.lang.Closure)

另请注意,将通过最新检查来检测和处理更改不可用文件的可用性(例如,将断开的符号链接的目标修改为有效文件,反之亦然).

任务的输入还用于计算构建高速缓存密钥,该密钥在启用后将加载任务输出. 有关更多详细信息,请参见任务输出缓存 .

为了跟踪任务,任务动作和嵌套输入的实现,Gradle使用类名称和包含实现的类路径标识符. 在某些情况下,Gradle无法准确跟踪实现:

Unknown classloader

当Gradle尚未创建加载实现的类加载器时,无法确定类路径.

Java lambda

Java lambda类是在运行时使用不确定的类名创建的. 因此,类名称不能标识lambda的实现,并且不能在不同的Gradle运行之间进行更改.

当无法精确跟踪任务,任务动作或嵌套输入的实现时,Gradle将禁用该任务的任何缓存. 这意味着该任务永远不会是最新的,也不会从构建缓存中加载.

Advanced techniques

到目前为止,您在本节中看到的所有内容都将涵盖您将遇到的大多数用例,但是有些情况需要特殊对待. 接下来,我们将为您提供其中一些解决方案.

Adding your own cached input/output methods

您是否想过Copy任务的from()方法如何工作? 它没有用@InputFiles注释,但是传递给它的任何文件都被视为任务的正式输入. 发生了什么?

实现非常简单,您可以针对自己的任务使用相同的技术来改进其API. 编写您的方法,以便它们将文件直接添加到适当的带注释的属性. 作为示例,以下是如何向我们前面介绍的自定义ProcessTemplates类添加一个sources()方法的方法:

Example 33. Declaring a method to add task inputs
build.gradle
task processTemplates(type: ProcessTemplates) {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData = new TemplateData("test", [year: 2012])
    outputDir = file("$buildDir/genOutput")

    sources fileTree("src/templates")
}
build.gradle.kts
tasks.register<ProcessTemplates>("processTemplates") {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData = TemplateData("test", mapOf("year" to "2012"))
    outputDir = file("$buildDir/genOutput")

    sources(fileTree("src/templates"))
}
ProcessTemplates.java
public class ProcessTemplates extends DefaultTask {
    // ...
    private FileCollection sourceFiles = getProject().getLayout().files();

    @SkipWhenEmpty
    @InputFiles
    @PathSensitive(PathSensitivity.NONE)
    public FileCollection getSourceFiles() {
        return this.sourceFiles;
    }

    public void sources(FileCollection sourceFiles) {
        this.sourceFiles = this.sourceFiles.plus(sourceFiles);
    }

    // ...
}
gradle processTemplates输出
> gradle processTemplates
> Task :processTemplates


BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

换句话说,只要您在配置阶段将值和文件添加到正式任务的输入和输出中,无论您在构建中的何处添加它们,它们都将被视为此类.

如果我们也希望支持任务作为参数,并将其输出作为输入,则可以像下面这样使用project.layout.files()方法:

例子34.声明一种添加任务作为输入的方法
build.gradle
task copyTemplates(type: Copy) {
    into "$buildDir/tmp"
    from "src/templates"
}

task processTemplates2(type: ProcessTemplates) {
    // ...
    sources copyTemplates
}
build.gradle.kts
val copyTemplates by tasks.registering(Copy::class) {
    into("$buildDir/tmp")
    from("src/templates")
}

tasks.register<ProcessTemplates>("processTemplates2") {
    // ...
    sources(copyTemplates.get())
}
ProcessTemplates.java
    // ...
    public void sources(Task inputTask) {
        this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask));
    }
    // ...
gradle processTemplates2输出
> gradle processTemplates2
> Task :copyTemplates
> Task :processTemplates2


BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

此技术可以使您的自定义任务更易于使用,并生成更干净的构建文件. 另外,使用getProject().getLayout().files()意味着我们的自定义方法可以设置推断的任务依赖项.

最后需要注意的一件事:如果要开发一个将源文件集合作为输入的任务,例如本示例,请考虑使用内置的SourceTask . 这将使您不必实施我们放入ProcessTemplates某些管道.

当您要将一个任务的输出链接到另一任务的输入时,类型通常匹配,并且简单的属性分配将提供该链接. 例如,可以将File输出属性分配给File输入.

不幸的是,当您希望一个任务的@OutputDirectory (类型为File )中的File成为另一个任务的@InputFiles属性(类型为FileCollection )的源时,这种方法会FileCollection . 由于这两种类型不同,因此属性分配将不起作用.

例如,假设您想将Java编译任务的输出(通过destinationDir属性)用作定制任务的输入,该定制任务检测一组包含Java字节码的文件. 这个自定义任务(我们称为Instrument )具有classFiles属性,该属性带有@InputFiles注释. 您最初可能会尝试像这样配置任务:

例子35.尝试建立推断的任务依赖项失败
build.gradle
plugins {
    id 'java'
}

task badInstrumentClasses(type: Instrument) {
    classFiles = fileTree(compileJava.destinationDir)
    destinationDir = file("$buildDir/instrumented")
}
build.gradle.kts
plugins {
    java
}

tasks.register<Instrument>("badInstrumentClasses") {
    classFiles = fileTree(tasks.compileJava.get().destinationDir)
    destinationDir = file("$buildDir/instrumented")
}
gradle clean badInstrumentClasses输出
> gradle clean badInstrumentClasses
> Task :clean UP-TO-DATE
> Task :badInstrumentClasses NO-SOURCE


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

这段代码显然没有错,但是您可以从控制台输出中看到缺少编译任务. 在这种情况下,您将需要通过dependsOninstrumentClassescompileJava之间添加显式任务依赖关系. 使用fileTree()意味着Gradle无法推断任务依赖项本身.

一种解决方案是使用TaskOutputs.files属性,如以下示例所示:

例子36.在输出目录和输入文件之间建立一个推断的任务依赖关系
build.gradle
task instrumentClasses(type: Instrument) {
    classFiles = compileJava.outputs.files
    destinationDir = file("$buildDir/instrumented")
}
build.gradle.kts
tasks.register<Instrument>("instrumentClasses") {
    classFiles = tasks.compileJava.get().outputs.files
    destinationDir = file("$buildDir/instrumented")
}
gradle clean instrumentClasses输出
> gradle clean instrumentClasses
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClasses


BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

另外,您可以通过使用project.files()project.layout.files()project.objects.fileCollection()之一代替project.fileTree()本身来访问适当的属性:

例子37.使用layout.files()设置一个推断的任务依赖项
build.gradle
task instrumentClasses2(type: Instrument) {
    classFiles = layout.files(compileJava)
    destinationDir = file("$buildDir/instrumented")
}
build.gradle.kts
tasks.register<Instrument>("instrumentClasses2") {
    classFiles = layout.files(tasks.compileJava.get())
    destinationDir = file("$buildDir/instrumented")
}
gradle clean instrumentClasses2输出
> gradle clean instrumentClasses2
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClasses2


BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

请记住, files()layout.files()objects.fileCollection()可以将任务作为参数,而fileTree()不能.

这种方法的缺点是,源任务的所有文件输出都将成为目标的输入文件-在这种情况下为instrumentClasses . 只要源任务只有一个基于文件的输出(如JavaCompile任务)就可以了. 但是,如果您只需要链接多个输出属性中的一个,则需要使用builtBy方法明确告诉Gradle哪个任务生成输入文件:

例子38.使用builtBy()设置一个推断的任务依赖项
build.gradle
task instrumentClassesBuiltBy(type: Instrument) {
    classFiles = fileTree(compileJava.destinationDir) {
        builtBy compileJava
    }
    destinationDir = file("$buildDir/instrumented")
}
build.gradle.kts
tasks.register<Instrument>("instrumentClassesBuiltBy") {
    classFiles = fileTree(tasks.compileJava.get().destinationDir) {
        builtBy(tasks.compileJava.get())
    }
    destinationDir = file("$buildDir/instrumented")
}
gradle clean instrumentClassesBuiltBy输出
> gradle clean instrumentClassesBuiltBy
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClassesBuiltBy


BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

当然,您可以通过dependsOn添加一个显式的任务依赖关系,但是上述方法提供了更多的语义含义,解释了为什么必须提前运行compileJava .

Providing custom up-to-date logic

Gradle自动处理输出文件和目录的最新检查,但是如果任务输出完全是其他东西怎么办? 也许这是对Web服务或数据库表的更新. 在这种情况下,Gradle无法知道如何检查任务是否最新.

在这upToDateWhen()的方法TaskOutputs进来.这需要用来确定任务是否是最新的,或者不是一个谓语功能. 一种用例是完全禁用一项任务的最新检查,如下所示:

例子39.忽略最新的检查
build.gradle
task alwaysInstrumentClasses(type: Instrument) {
    classFiles = layout.files(compileJava)
    destinationDir = file("$buildDir/instrumented")
    outputs.upToDateWhen { false }
}
build.gradle.kts
tasks.register<Instrument>("alwaysInstrumentClasses") {
    classFiles = layout.files(tasks.compileJava.get())
    destinationDir = file("$buildDir/instrumented")
    outputs.upToDateWhen { false }
}
Output of gradle clean alwaysInstrumentClasses
> gradle clean alwaysInstrumentClasses
> Task :compileJava
> Task :alwaysInstrumentClasses


BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date
gradle alwaysInstrumentClasses输出gradle alwaysInstrumentClasses
> gradle alwaysInstrumentClasses
> Task :compileJava UP-TO-DATE
> Task :alwaysInstrumentClasses


BUILD SUCCESSFUL in 0s
2 actionable tasks: 1 executed, 1 up-to-date

{ false }闭包确保将始终执行alwaysInstrumentClasses ,而不管输入或输出是否不变.

您当然可以在闭包中添加更复杂的逻辑. 例如,您可以检查数据库表中的特定记录是否存在或已更改. 请注意,最新的检查应该可以节省您的时间. 不要添加比标准执行任务花费更多或更多时间的检查. 实际上,如果某个任务由于很少更新而最终经常运行,则根本不值得进行最新检查. 请记住,如果任务在执行任务图中,则检查将始终运行.

一个常见的错误是使用upToDateWhen()而不是Task.onlyIf() . 如果要基于与任务输入和输出无关的某些条件跳过任务,则应使用onlyIf() . 例如,在要设置或不设置特定属性的情况下要跳过任务的情况.

Configure input normalization

对于最新检查和构建缓存, Gradle需要确定两个任务输入属性是否具有相同的值. 为此,Gradle首先将两个输入标准化,然后比较结果. 例如,对于编译类路径,Gradle从类路径上的类中提取ABI签名,然后按照Java编译规避中的描述比较上一次Gradle运行和当前Gradle运行之间的签名.

可以自定义Gradle的内置策略以用于运行时类路径规范化. 用@ Classpath注释的所有输入都被视为运行时类路径.

假设您要向所有生成的jar文件中添加一个文件build-info.properties ,其中包含有关构建的信息,例如,构建开始时的时间戳或一些ID,以标识发布该工件的CI作业. 该文件仅用于审计目的,对运行测试的结果没有影响. 尽管如此,该文件还是test任务的运行时类路径的一部分,并且在每次构建调用时都会更改. 因此, test永远不会是最新的,也不会从构建缓存中提取. 为了再次从增量构建中受益,您可以通过使用Project.normalization(org.gradle.api.Action) (在使用项目中)告诉Gradle在项目级别的运行时类路径上忽略此文件:

例子40.运行时类路径规范化
build.gradle
normalization {
    runtimeClasspath {
        ignore 'build-info.properties'
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        ignore("build-info.properties")
    }
}

如果将这样的文件添加到jar文件中是您对构建中的所有项目执行的操作,并且想要为所有使用者过滤此文件,则可以将上述配置包装在allprojects {}subprojects {}块中在根构建脚本中.

此配置的效果是,对于最新检查和构建缓存键计算,将忽略对build-info.properties更改. 请注意,这不会改变test任务的运行时行为-即,任何测试仍然能够加载build-info.properties ,并且运行时类路径仍与以前相同.

Stale task outputs

当Gradle版本更改时,Gradle将检测到需要删除使用较旧版本的Gradle运行的任务的输出,以确保任务的最新版本从已知的干净状态开始.

仅针对源集的输出(Java / Groovy / Scala编译)实现了对陈旧的输出目录的自动清除.

Task rules

有时您想执行一个任务,该任务的行为取决于较大或无限数量的参数值范围. 提供此类任务的一种非常好的表达方式是任务规则:

例子41.任务规则
build.gradle
tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}
build.gradle.kts
tasks.addRule("Pattern: ping<ID>") {
    val taskName = this
    if (startsWith("ping")) {
        task(taskName) {
            doLast {
                println("Pinging: " + (taskName.replace("ping", "")))
            }
        }
    }
}
gradle -q pingServer1输出
> gradle -q pingServer1
Pinging: Server1

String参数用作规则的描述,与gradle tasks一起gradle tasks .

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

例子42.对基于规则的任务的依赖
build.gradle
tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}

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

task("groupPing") {
    dependsOn("pingServer1", "pingServer2")
}
gradle -q groupPing输出
> gradle -q groupPing
Pinging: Server1
Pinging: Server2

如果运行" gradle -q tasks ",将找不到名为" pingServer1 "或" pingServer2 "的任务,但是此脚本正在根据运行这些任务的请求执行逻辑.

Finalizer tasks

当计划运行终结任务时,终结任务会自动添加到任务图中.

例子43.添加一个任务终结器
build.gradle
task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}

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

taskX { finalizedBy(taskY) }
gradle -q taskX输出
> gradle -q taskX
taskX
taskY

即使完成任务失败,也将执行终结器任务.

例子44.失败任务的任务终结器
build.gradle
task taskX {
    doLast {
        println 'taskX'
        throw new RuntimeException()
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}

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

taskX { finalizedBy(taskY) }
gradle -q taskX输出
> gradle -q taskX
taskX
taskY

FAILURE: Build failed with an exception.

* Where:
Build file '/home/user/gradle/samples/groovy/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

另一方面,如果已完成任务没有执行任何工作(例如,如果认为已完成任务或从属任务失败),则不会执行完成任务.

在构建创建无论构建失败还是成功都必须清除的资源的情况下,终结器任务很有用. 这种资源的一个示例是一个Web容器,它在集成测试任务之前启动,并且即使某些测试失败,也应始终将其关闭.

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

Lifecycle tasks

生命周期任务是不能自行工作的任务. 他们通常没有任何任务动作. 生命周期任务可以代表几个概念:

  • 工作流程步骤(例如,使用check运行所有检查)

  • 可构建的东西(例如,使用debug32MainExecutable为本机组件创建调试的32位可执行文件)

  • 执行许多相同逻辑任务的便捷任务(例如,使用compileAll运行所有编译任务)

基本插件定义了一些标准的生命周期任务 ,如buildassemblecheck . 所有核心语言插件(如Java插件 )均应用基本插件,因此具有相同的生命周期任务基本集.

除非生命周期任务具有操作,否则其结果取决于其任务依赖性. 如果执行了这些依赖关系中的任何一个,则生命周期任务将被视为EXECUTED . 如果所有任务相关性都是最新的,已跳过或已从缓存中删除,则将生命周期任务视为UP-TO-DATE .

Summary

如果您来自Ant,则增强的Gradle任务(如Copy)似乎是Ant目标与Ant任务之间的交叉. 尽管Ant的任务和目标实际上是不同的实体,但是Gradle将这些概念组合为一个实体. 简单的Gradle任务就像Ant的目标一样,但是增强的Gradle任务也包括Ant任务的各个方面. Gradle的所有任务共享一个通用的API,您可以在它们之间创建依赖关系. 这些任务比Ant任务更容易配置. 它们充分利用了类型系统,并且更具表现力且易于维护.


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