深入了解 Tasks

在这本教程的一开始 (第 6 章, 构建脚本基础) 你已经学习了如何创建简单的任务. 然后你也学习了如何给这些任务加入额外的行为, 以及如何在任务之间建立依赖关系. 这些仅仅是用来构建简单的任务. Gradle 可以创建更为强大复杂的任务. 这些任务可以有它们自己的属性和方法. 这一点正是和 Ant targets 不一样的地方. 这些强大的任务既可以由你自己创建也可以使用 Gradle 内建好的.

定义 tasks

我们已经在第 6 章学习了定义任务的形式 (keyword 形式). 当然也会有一些定义形式的变化来适应某些特殊的情况. 比如下面的例子中任务名被括号括起来了. 这是因为之前定义简单任务的形式 (keyword 形式) 在表达式里是不起作用的.

例子 15.1. 定义 tasks

build.gradle

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

task(copy, type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}

你也可以使用 strings 来定义任务的名字:

例子 15.2. 例子 tasks - 使用 strings 来定义任务的名字

build.gradle

task('hello') <<
{
    println "hello"
}

task('copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}

还有另外一种语法形式来定义任务, 更加直观:

例子 15.3. 另外一种语法形式

build.gradle

tasks.create(name: 'hello') << {
    println "hello"
}

tasks.create(name: 'copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}

这里实际上我们把任务加入到 tasks collection 中. 可以看一看 TaskContainer 来深入了解下.

定位 tasks

你经常需要在构建文件里找到你定义的 tasks, 举个例子, 为了配置它们或者使用它们作为依赖. 有许多种方式都可以来实现定位. 首先, 每一个任务都必须是一个 project 的有效属性, 并使用任务名来作为属性名:

例子 15.4. 通过属性获取 tasks

build.gradle

task hello

println hello.name
println project.hello.name

Tasks 也可以通过 tasks collection 来得到.

例子 15.5. 通过 tasks collection 获取 tasks

build.gradle

task hello

println tasks.hello.name
println tasks['hello'].name

你也可以使用 tasks.getByPath() 方法通过任务的路径来使用任何 project 里的任务. 你可以通过使用任务的名字, 任务的相对路径或者绝对路径作为 getByPath() 方法的输入.

例子 15.6. 通过路径获取 tasks

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

gradle -q hello 的输出

> gradle -q hello
:hello
:hello
:projectA:hello
:projectA:hello

参考 TaskContainer 可以知道跟多关于定位 tasks 的选项.

配置 tasks

举一个例子, 让我们看一看 Gradle 自带的 Copy task. 为了创建一个 Copy task, 你需要在你的构建脚本里先声明它:

例子 15.7. 创建一个 copy task

build.gradle

task myCopy(type: Copy)

它创建了一个没有默认行为的 copy task. 这个 task 可以通过它的 API 来配置(参考 Copy). 接下来例子展示了不同的实现方法.

补充说明一下, 这个 task 的名字是 “myCopy”, 但是它是 “Copy” 类(type). 你可以有许多同样 type 不同名字的 tasks. 这个在实现特定类型的所有任务的 cross-cutting concerns 时特别有用.

例子 15.8. 配置一个任务 - 不同的方法

build.gradle

Copy myCopy = task(myCopy, type: Copy)
myCopy.from 'resources'
myCopy.into 'target'
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')

这个我们通过 Java 配置对象是一样的形式. 但是你每次都必须在语句里重复上下文 (myCopy). 这种方式可能读起来并不是那么的漂亮.

下面一种方式就解决了这个问题. 是公认的最具可读性的方式.

例子 15.9. 配置一个任务 - 通过闭包 closure

build.gradle

task myCopy(type: Copy)

myCopy {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

上面例子里的第三行是 tasks.getByName() 方法的一个简洁的写法. 特别要注意的是, 如果你通过闭包的形式来实现 getByName() 方法, 这个闭包会在 task 配置的时候执行而不是在 task 运行的时候执行.

你也可以直接在定义 task 时使用闭包.

例子 15.10. 通过定义一个任务

build.gradle

task copy(type: Copy) {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

请不要忘了构建的各个阶段.

一个任务有配置和动作. 当使用 << 时, 你只是简单的使用捷径定义了动作. 定义在配置区域的代码只会在构建的配置阶段执行, 而且不论执行哪个任务. 可以参考第 55 章, The Build Lifecycle for more details about the build lifecycle.

给 task 加入依赖

有许多种加入依赖的方式. 在 6.5 小节, “任务依赖”里, 你已经学习了如何使用任务的名称定义依赖. 任务名称可以指向同一个项目里的任务, 或者其他项目里的任务. 为了指向其他项目, 你必须在任务的名称前加入项目的路径. 下面的例子给 projectA:taskX 加入依赖 projectB:taskY :

例子 15.11. 从另外一个项目给任务加入依赖

build.gradle

project('projectA') {
    task taskX(dependsOn: ':projectB:taskY') << {
        println 'taskX'
    }
}

project('projectB') {
    task taskY << {
        println 'taskY'
    }
}

gradle -q taskX 的输出

> gradle -q taskX
taskY
taskX

除了使用任务名称, 你也可以定义一个依赖对象y:

例子 15.12. 通过任务对象加入依赖

build.gradle

task taskX << {
    println 'taskX'
}

task taskY << {
    println 'taskY'
}

taskX.dependsOn taskY

gradle -q taskX 的输出

> gradle -q taskX
taskY
taskX

更加先进的用法, 你可以通过闭包定义一个任务依赖. 闭包只能返回一个单独的 Task 或者 Task 对象的 collection, 这些返回的任务就将被当做依赖. 接下来的例子给 taskX 加入了一个复杂的依赖, 所有以 lib 开头的任务都将在 taskX 之前执行:

例子 15.13. 通过闭包加入依赖

build.gradle


task taskX << {
    println 'taskX'
}

taskX.dependsOn {
    tasks.findAll { task -> task.name.startsWith('lib') }
}

task lib1 << {
    println 'lib1'
}

task lib2 << {
    println 'lib2'
}

task notALib << {
    println 'notALib'
}

gradle -q taskX 的输出

> gradle -q taskX
lib1
lib2
taskX

For more information about task dependencies, see the Task API.

给 tasks 排序

任务的排序功能正在测试和优化. 请注意, 这项功能在 Gradle 之后的版本里可能会改变.

在某些情况下, 我们希望能控制任务的的执行顺序, 这种控制并不是向上一张那样去显示地加入依赖关系. 最主要的区别是我们设定的排序规则不会影响那些要被执行的任务, 只是影响执行的顺序本身. 好吧, 我知道可能有点抽象.

我们来看看以下几种有用的场景:

  • 执行连续的任务: eg. 'build' 从来不会在 'clean' 之前执行.
  • 在 build 的一开始先运行构建确认 (build validations): eg. 在正式的发布构建前先确认我的证书是正确的.
  • 在运行长时间的检测任务前先运行快速的检测任务来获得更快的反馈: eg. 单元测试总是应该在集成测试之前被执行.
  • 一个聚集 (aggregates) 某种特定类型的所有任务结果的任务: eg. 测试报告任务 (test report task) 包含了所有测试任务的运行结果.

目前, 有 2 种可用的排序规则: "must run after""should run after".

当你使用 “must run after” 时即意味着 taskB 必须总是在 taskA 之后运行, 无论 taskA 和 taskB 是否将要运行:

taskB.mustRunAfter(taskA)

"should run after" 规则其实和 "must run after" 很像, 只是没有那么的严格, 在 2 种情况下它会被忽略:

  1. 使用规则来阐述一个执行的循环.
  2. 当并行执行并且一个任务的所有依赖除了 “should run after” 任务其余都满足了, 那么这个任务无论它的 “should run after” 依赖是否执行, 它都可以执行. (编者: 翻译待商榷, 提供具体例子)

总之, 当要求不是那么严格时, “should run after” 是非常有用的.

即使有目前的这些规则, 我们仍可以执行 taskA 而不管 taskB, 反之亦然.

例子 15.14. 加入 'must run after'

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskY.mustRunAfter taskX

gradle -q taskY taskX 的输出

> gradle -q taskY taskX
taskX
taskY

例子 15.15. 加入 'should run after'

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskY.shouldRunAfter taskX

gradle -q taskY taskX 的输出

> gradle -q taskY taskX
taskX
taskY

在上面的例子里, 我们仍可以直接执行 taskY 而不去 taskX :

例子 15.16. 任务排序不影响任务执行

gradle -q taskY 的输出

> gradle -q taskY
taskY

为了在 2 个任务间定义 “must run after” 或者 “should run after” 排序, 我们需要使用 Task.mustRunAfter() 和 Task.shouldRunAfter() 方法. 这些方法接收一个任务的实例, 任务的名字或者任何 Task.dependsOn()可以接收的输入.

注意 “B.mustRunAfter(A)” 或者 “B.shouldRunAfter(A)” 并不影响任何任务间的执行依赖:

  • tasks A 和 B 可以被独立的执行. 排序规则只有当 2 个任务同时执行时才会被应用.
  • 在运行时加上 --continue, 当 A 失败时 B 仍然会执行.

之前提到过, “should run after” 规则在一个执行循环中将被忽略:

例子 15.17. 'should run after' 任务的忽略

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
task taskZ << {
    println 'taskZ'
}
taskX.dependsOn taskY
taskY.dependsOn taskZ
taskZ.shouldRunAfter taskX

gradle -q taskX 的输出

> gradle -q taskX
taskZ
taskY
taskX

给 task 加入描述

你可以给你的任务加入一段描述性的文字. 它将会在任务执行的时候显示出来.

例子 15.18. 给任务加入描述

build.gradle

task copy(type: Copy) {
   description 'Copies the resource directory to the target directory.'
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

替换 tasks

有时候你想要替换一个任务. 举个例子, 如果你想要互换一个通过 java 插件定义的任务和一个自定义的不同类型的任务:

例子 14.19. 覆写一个任务

build.gradle

task copy(type: Copy)

task copy(overwrite: true) << {
    println('I am the new one.')
}

gradle -q copy 的输出

> gradle -q copy
I am the new one.

这种方式将用你自己定义的任务替换一个 Copy 类型的任务, 因为它使用了同样的名字. 但你定义一个新的任务时, 你必须设置 overwrite 属性为 true. 否则的话 Gradle 会抛出一个异常, task with that name already exists.

跳过 tasks

Gradle 提供了好几种跳过一个任务的方式.

1. 使用判断条件 (predicate)

你可以使用 onlyIf() 方法来为一个任务加入判断条件. 就和 Java 里的 if 语句一样, 任务只有在条件判断为真时才会执行. 你通过一个闭包来实现判断条件. 闭包像变量一样传递任务, 如果任务应该被执行则返回真, 反之亦然. 判断条件在任务执行之前进行判断.

例子 15.20. 使用判断条件跳过一个任务

build.gradle

task hello << {
    println 'hello world'
}

hello.onlyIf { !project.hasProperty('skipHello') }

gradle hello -PskipHello 的输出

> gradle hello -PskipHello
:hello SKIPPED
BUILD SUCCESSFUL

Total time: 1 secs

2. 使用 StopExecutionException

如果想要跳过一个任务的逻辑并不能被判断条件通过表达式表达出来, 你可以使用 StopExecutionException. 如果这个异常是被一个任务要执行的动作抛出的, 这个动作之后的执行以及所有紧跟它的动作都会被跳过. 构建将会继续执行下一个任务.

例子 15.21. 通过 StopExecutionException 跳过任务

build.gradle

task compile << {
    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') << {
   println 'I am not affected'
}

gradle -q myTask 的输出

> gradle -q myTask
I am not affected

如果你直接使用 Gradle 提供的任务, 这项功能还是十分有用的. 它允许你为内建的任务加入条件来控制执行. [6]

3. 激活和注销 tasks

每一个任务都有一个已经激活的标记(enabled flag), 这个标记一般默认为真. 将它设置为假, 那它的任何动作都不会被执行.

例子 15.22. 激活和注销 tasks

build.gradle

task disableMe << {
    println 'This should not be printed if the task is disabled.'
}
disableMe.enabled = false

gradle disableMe 的输出

> gradle disableMe
:disableMe SKIPPED

BUILD SUCCESSFUL

Total time: 1 secs

跳过 up-to-date 的任务

如果你正在使用一些附加的任务, 比如通过 Java 插件加入的任务, 你可能会注意到 Gradle 会跳过一些任务, 这些任务后面会标注 up-to-date. 代表这个任务已经运行过了或者说是最新的状态, 不再需要产生一次相同的输出. 不仅仅是这些内建任务, 其实你在运行自己的任务时, 也会碰到这种情况.

1. 声明一个任务的输入和输出

让我们先看一个例子. 这里我们的任务会根据一个 XML 文件生成好几个输出文件. 让我们运行这个任务 2 次.

例子 15.23. A generator task

build.gradle

task transform {
    ext.srcFile = file('mountains.xml')
    ext.destDir = new File(buildDir, 'generated')
    doLast {
        println "Transforming source file."
        destDir.mkdirs()
        def mountains = new XmlParser().parse(srcFile)
        mountains.mountain.each { mountain ->
            def name = mountain.name[0].text()
            def height = mountain.height[0].text()
            def destFile = new File(destDir, "${name}.txt")
            destFile.text = "$name -> ${height}\n"
        }
    }
}

gradle transform 的输出

> gradle transform
:transform
Transforming source file.

gradle transform 的输出

> gradle transform
:transform
Transforming source file.

这里 Gradle 执行了这个任务两次, 即使什么都没有改变, 它也没有跳过这个任务. 这个例子里的任务, 它的行为是通过闭包定义的. Gradle 并不知道闭包会做什么, 也并不能自动指出是否这个任务是 up-to-date. 为了使用 Gradle 的 up-to-date 检测, 你需要定义任务的输入和输出.

每个任务都有输入和输出属性, 你需要使用这些属性来声明任务的输入和输出. 下面的例子中, 我们将声明 XML 文件作为输入, 并且把输出放在一个指定的目录. 让我们运行这个任务 2 次.

例子 15.24. 声明任务的输入和输出

build.gradle

task transform {
    ext.srcFile = file('mountains.xml')
    ext.destDir = new File(buildDir, 'generated')
    inputs.file srcFile
    outputs.dir destDir
    doLast {
        println "Transforming source file."
        destDir.mkdirs()
        def mountains = new XmlParser().parse(srcFile)
        mountains.mountain.each { mountain ->
            def name = mountain.name[0].text()
            def height = mountain.height[0].text()
            def destFile = new File(destDir, "${name}.txt")
            destFile.text = "$name -> ${height}\n"
        }
    }
}

gradle transform 的输出

> gradle transform
:transform
Transforming source file.

gradle transform 的输出

> gradle transform
:transform UP-TO-DATE

现在, Gradle 就能够检测出任务是否是 up-to-date 状态.

任务的输入属性是 TaskInputs 类型. 任务的输出属性是 TaskOutputs 类型.

一个任务如果没有定义输出的话, 那么它永远都没用办法判断 up-to-date. 对于某些场景, 比如一个任务的输出不是文件, 或者更复杂的场景, TaskOutputs.upToDateWhen() 方法会计算任务的输出是否应被视为最新.

总而言之, 如果一个任务只定义了输出, 如果输出不变的话, 它就会被视为 up-to-date.

2. 它是如何工作的?

当一个任务是首次执行时, Gradle 会取一个输入的快照 (snapshot). 该快照包含组输入文件和每个文件的内容的散列. 然后当 Gradle 执行任务时, 如果任务成功完成,Gradle 会获得一个输出的快照. 该快照包含输出文件和每个文件的内容的散列. Gradle 会保留这两个快照用来在该任务的下一次执行时进行判断.

之后, 每次在任务执行之前, Gradle 都会为输入和输出取一个新的快照, 如果这个快照和之前的快照一样, Gradle 就会假定这个任务已经是最新的 (up-to-date) 并且跳过任务, 反之亦然.

需要注意的是, 如果一个任务有指定的输出目录, 自从该任务上次执行以来被加入到该目录的任务文件都会被忽略, 并且不会引起任务过时 (out of date). 这是因为不相关任务也许会共用同一个输出目录. 如果这并不是你所想要的情况, 可以考虑使用 TaskOutputs.upToDateWhen()

Task 规则

有时候也想要一个任务的行为是基于已经定义好的取值范围或者特定规则, 下面的例子就提供了一种很直观漂亮的方式:

例子 15.25. 任务规则

build.gradle

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) << {
            println "Pinging: " + (taskName - 'ping')
        }
    }
}

gradle -q pingServer1 的输出

> gradle -q pingServer1
Pinging: Server1

这里的 String 参数就是用来定义规则的.

规则并不只是在通过命令行使用任务的时候执行. 你也可以基于规则来创建依赖关系:

例子 15.26. 基于规则的任务依赖

build.gradle

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) << {
            println "Pinging: " + (taskName - 'ping')
        }
    }
}

task groupPing {
    dependsOn pingServer1, pingServer2
}

gradle -q groupPing 的输出

> gradle -q groupPing
Pinging: Server1
Pinging: Server2

如果你运行 “gradle -q tasks”, 你并不能找到名叫 “pingServer1” 或者 “pingServer2” 的任务, 但是这个脚本仍然会执行这些任务.

终止 tasks

终止任务是一个正在开发的功能.

这里的终止任务并不是指终止一个任务, 而是指一个无论运行结果如何最后都会被执行的任务.

例子 15.27. 加入一个任务终止器

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}

taskX.finalizedBy taskY

gradle -q taskX 的输出

> gradle -q taskX
taskX
taskY

即使要终止的任务失败了, 终止任务仍会继续执行.

例子 14.28. 当任务失败时k

build.gradle

task taskX << {
    println 'taskX'
    throw new RuntimeException()
}
task taskY << {
    println 'taskY'
}

taskX.finalizedBy taskY

gradle -q taskX 的输出

> gradle -q taskX
taskX
taskY

另外, 如果要终止的任务并没有被执行 (比如上一节讲的 up-to-date) 那么终止任务并不会执行.

当构建创建了一个资源, 无论构建失败或成功时这个资源必须被清除的时候, 终止任务就非常有用.

要使用终止任务, 你必须使用 Task.finalizedBy() 方法. 一个任务的实例, 任务的名称, 或者任何 Task.dependsOn() 可以接收的输入都可以作为这个任务的输入.

补充

下面补充的部分原本是第 14 章,最新的 Gradle 文档将其移除,所以将其作为补充放到这一章节。

Gradle 属性 和 system 属性

Gradle 提供了多种的方法让您可以在构建脚本中添加属性. 使用 -D 命令选项,您可以向运行 Gradle 的 JVM 传递一个 system 属性 . Gradle 命令的 -D 选项 和 Java 命令的 -D 选项有些相同的效果.

您也可以使用属性文件向您的 Project 对象中添加属性. 您可以在 Gradle 用户目录( 如果您没有在 USER_HOME/.gradle 配置默认设置,则由"GRADLE_USER_HOME" 环境变量定义) 或者项目目录放置一个 gradle.properties 文件.如果是多项目的话,您可以在每个子目录里都放置一个 gradle.properties 文件. gradle.properties 文件内容里的属性能够被 Project 对象访问到. 不过有一点,用户目录中的 gradle.properties 文件优先权大于项目目录中的 gradle.properties 文件.

您也可以通过 -P 命令选项直接向Project 对象中添加属性.

另外,当 Gradle 看到特别命名的 system 属性或者环境变量时,Gradle 也可以设置项目属性. 比如当您没有管理员权限去持续整合服务,还有您需要设置属性值但是不容易时,这个特性非常有用. 出于安全的原因,在这种情况下,您没法使用 -P 命令选项,您也不能修改系统级别的文件. 确切的策略是改变您持续继承构建工作的配置,增加一个环境变量设置令它匹配一个期望的模式. 对于当前系统来说,这种方法对于普通用户来说是不可见的. [[6]](#md-anchor)

如果环境变量的名字是 ORG_GRADLE_PROJECT=somevalue, Gradle 会使用值为 somevalue 在您的 Project 对象中设定一个支持属性. 另外 Gradle 也支持 system 属性,但是使用不同的名字模式,例如 org.gradle.project.prop .

您也可以在 gradle.properties 文件中设置 system 属性.如果一个属性名的前缀为 “systemProp”,那么这个属性和它的属性值会被设置为 system 属性. 如果没有这个前缀,在多项目构建中,除了根项目会被忽略外,“systemProp.” 属性会在任何项目中设置.也就是说仅仅根项目的 gradle.properties 文件会被检查其属性的前缀是否是 “systemProp”.

**例子 14.2.通过 gradle.properties 文件设置属性

gradle.properties

gradlePropertiesProp=gradlePropertiesValue
sysProp=shouldBeOverWrittenBySysProp
envProjectProp=shouldBeOverWrittenByEnvProp
systemProp.system=systemValue

build.gradle

task printProps << {
    println commandLineProjectProp
    println gradlePropertiesProp
    println systemProjectProp
    println envProjectProp
    println System.properties['system']

}

[6]. Jenkins, Teamcity, or Bamboo 都是 提供这个功能的 CI 服务.

使用 gradle -q -PcommandLineProjectProp=commandLineProjectPropValue -Dorg.gradle.project.systemProjectProp=systemPropertyValue printProps 输出

> gradle -q -PcommandLineProjectProp=commandLineProjectPropValue    -Dorg.gradle.project.systemProjectProp=systemPropertyValue printProps
commandLineProjectPropValue
gradlePropertiesValue
systemPropertyValue
envPropertyValue
systemValue

使用其他的脚本配置项目

您还可以使用其他的构建脚本来配置当前的项目,Gradle 构建语言的所有的内容对于其他的脚本都是可以使用的. 您甚至可以在别的脚本中再使用其他的脚本.

例子 14.3.使用其他的构建脚本配置项目

build.gradle

apply from: 'other.gradle'

other.gradle

println "configuring $project"
task hello << {
    println' 'hello form other srcipt'
}

使用 gradle -q hello 输出

> gradle -q hello
configuring root project 'configureProjectUsingScript'
hello from other script

使用其他的脚本配置任意对象

您也可以使用其他的构建脚本配置任意的对象.

例子: 14.5.使用别的脚本配置配置对象

build.gradle

task config << {
   def pos = new java.text.FieldPosition(10)

   // 使用另一个脚本
   apply from: 'other.gradle', to: pos
   println pos.beginIndex
   println pos.endIndex

}

other.gradle

beginIndex = 1
endIndex = 5

使用 gradle -q configure 输出

> gradle -q configure
1
5

配置任意对象

您可以使用下面方法配置任意的对象.

例子 14.4.配置任意对象

build.gradle

task configure << {
   def pos = configure(new java.text.FieldPosition(10)) {
       beginIndex = 1
       endIndex = 5
   }

   println pos.beginIndex
   println pos.endIndex

}

使用 gradle -q configure 输出

> gradle -q configure
1
5

缓存

为了提高响应能力,Gradle 默认缓存了所有编译后的脚本. 包括所有的构建脚本,初始化脚本,还有其他脚本. Gradle 创建了一个 .gradle 目录来存放编译后的脚本,下次您运行构建脚本时,如果这个脚本自从它被编译后就再也没有被改动过,Gradle 会先使用编译后的脚本. 否则 Gradle 会重新编译脚本,然后将新编译后的文件缓存起来. 如果您使用 Gradle --recompile--scripts 运行脚本,缓存的脚本就会被删除,然后新编译后的文件就会再被缓存. 这种方法可以强制 Gradle 重新编译脚本并缓存.