从Gradle 5.1开始,我们建议在创建任务时使用配置避免API。

writing tasks 4

任务配置避免API

如果任务在构建中不会被使用,配置避免API会避免配置它们,这可以显著影响总配置时间。

例如,当运行一个 `compile` 任务(应用了 `java` 插件)时,其他不相关的任务(如 `clean`、`test`、`javadocs`)将不会被执行。

为了避免创建和配置构建不需要的任务,我们可以 注册 该任务。

当一个任务被注册时,它对于构建是已知的。它可以被配置,并且可以传递对它的引用,但是任务对象本身尚未创建,并且其操作也尚未执行。注册的任务将保持这种状态,直到构建中的某些内容需要实例化任务对象。如果任务对象永远不需要,任务将保持注册状态,并且可以避免创建和配置任务的成本。

在Gradle中,您可以使用 TaskContainer.register(java.lang.String) 注册一个任务。 `register(…​)` 方法不返回任务实例,而是返回一个 TaskProvider,它是一个任务的引用,可以在许多通常使用任务对象的地方使用(例如,创建任务依赖项时)。

指南

延迟任务创建

有效的任务配置避免要求构建作者将 TaskContainer.create(java.lang.String) 的实例更改为 TaskContainer.register(java.lang.String)

Gradle的旧版本只支持 `create(…​)` API。 `create(…​)` API在调用时会急切地创建和配置任务,应该避免使用。

仅仅使用 `register(…​)` 可能不足以完全避免所有任务配置。您可能需要更改其他通过名称或类型配置任务的代码,详见下文。

延迟任务配置

DomainObjectCollection.all(org.gradle.api.Action)DomainObjectCollection.withType(java.lang.Class, org.gradle.api.Action) 这样的急切API会立即创建并配置任何已注册的任务。要延迟任务配置,您必须迁移到配置避免API的等效项。请参阅 下表 以确定最佳替代方案。

引用已注册的任务

除了引用任务对象,您还可以通过 TaskProvider 对象来操作已注册的任务。TaskProvider 可以通过多种方式获得,包括 TaskContainer.register(java.lang.String)TaskCollection.named(java.lang.String) 方法。

调用 Provider.get() 或使用 TaskCollection.getByName(java.lang.String) 按名称查找任务将导致任务被创建和配置。

Task.dependsOn(java.lang.Object…​)ConfigurableFileCollection.builtBy(java.lang.Object...) 这样的方法与 TaskProvider 的工作方式与 Task 相同,因此您无需解包 `Provider` 即可使显式依赖项继续工作。

您必须使用配置避免的等效方法来按名称配置任务。请参阅下表以确定最佳替代方案。

引用任务实例

如果您需要访问任务实例,可以使用 TaskCollection.named(java.lang.String)Provider.get()。这将导致任务被创建和配置,但一切都应像使用急切API时一样工作。

使用配置避免进行任务排序

调用排序方法本身不会导致任务创建。所有这些方法都只是声明关系。

这些关系的存在可能在构建过程的后期阶段间接导致任务创建。

当需要建立任务关系(即 `dependsOn`、`finalizedBy`、`mustRunAfter`、`shouldRunAfter`)时,可以区分软关系和强关系。它们在配置阶段对任务创建的影响不同

  • Task.mustRunAfter(…​)Task.shouldRunAfter(…​) 代表软关系,它们只能改变现有任务的顺序,但不能触发它们的创建。

  • Task.dependsOn(…​)Task.finalizedBy(…​) 代表强关系,它们 强制 执行引用的任务,即使它们本来没有被创建。

  • 如果一个任务 被执行,无论它是使用 Task.register(…​) 还是 Task.create(…​) 创建的,所定义的关系在配置时都不会触发任务创建。

  • 如果一个任务 执行,所有强关联的任务都必须在配置时创建和配置,因为它们可能具有其他 `dependsOn` 或 `finalizedBy` 关系。这将递归地发生,直到任务图包含所有强关系。

迁移指南

以下章节将介绍一些在迁移构建逻辑时应遵循的一般准则。我们还提供了一些建议的步骤,以及故障排除常见陷阱

迁移指南

  1. 在迁移过程中使用 `help` 任务作为基准。
    `help` 任务是衡量迁移过程的完美候选。在一个只使用配置避免API的构建中,Build Scan 显示在配置期间没有任务被创建,并且只创建了已执行的任务。

  2. 只在配置操作中修改当前任务。
    因为任务配置操作现在可以立即运行,稍后运行或永不运行,所以修改当前任务以外的任何内容都可能导致构建中出现不确定的行为。考虑以下代码

    val check by tasks.registering
    tasks.register("verificationTask") {
        // Configure verificationTask
    
        // Run verificationTask when someone runs check
        check.get().dependsOn(this)
    }
    def check = tasks.register("check")
    tasks.register("verificationTask") { verificationTask ->
        // Configure verificationTask
    
        // Run verificationTask when someone runs check
        check.get().dependsOn verificationTask
    }

    执行 `gradle check` 任务应该执行 `verificationTask`,但在这个例子中,它不会。这是因为 `verificationTask` 和 `check` 之间的依赖关系只在 `verificationTask` 实现时发生。为了避免此类问题,您必须只修改与配置操作关联的任务。其他任务应在其自己的配置操作中修改

    val check by tasks.registering
    val verificationTask by tasks.registering {
        // Configure verificationTask
    }
    check {
        dependsOn(verificationTask)
    }
    def check = tasks.register("check")
    def verificationTask = tasks.register("verificationTask") {
        // Configure verificationTask
    }
    check.configure {
        dependsOn verificationTask
    }

    将来,Gradle会将这种反模式视为错误并抛出异常。

  3. 优先进行小规模的增量更改。
    较小的更改更容易进行健全性检查。如果您破坏了构建逻辑,分析自上次成功验证以来的更改日志将更容易。

  4. 确保制定了验证构建逻辑的良好计划。
    通常,简单的 `build` 任务调用即可验证您的构建逻辑。但是,某些构建可能需要额外的验证——了解您的构建行为并确保您有一个良好的验证计划。

  5. 优先进行自动化测试而非手动测试。
    使用 TestKit 为您的构建逻辑编写集成测试是一个好习惯。

  6. 避免按名称引用任务。
    通常,按名称引用任务是一种脆弱的模式,应避免使用。尽管任务名称在 `TaskProvider` 上可用,但应努力使用强类型模型中的引用。

  7. 尽可能多地使用新的任务API。
    急切地实例化某些任务可能会导致其他任务级联实例化。使用 `TaskProvider` 有助于创建间接层,从而防止传递性实例化。

  8. 如果您尝试从新API的配置块中访问某些API,则可能会被禁止。
    例如,在配置使用新API注册的任务时,无法调用 `Project.afterEvaluate()`。由于 `afterEvaluate` 用于延迟配置 `Project`,将延迟配置与新API混合使用可能会导致难以诊断的错误,因为使用新API注册的任务并不总是被配置,但 `afterEvaluate` 块可能总是期望执行。

迁移步骤

迁移过程的第一部分是遍历代码并手动迁移急切的任务创建和配置,以使用配置避免API。

  1. 迁移影响所有任务(`tasks.all {}`)或按类型子集(`tasks.withType(…​) {}`)的任务配置。
    这将导致您的构建在插件注册任务时,急切创建的任务更少。

  2. 迁移按名称配置的任务。
    这将导致您的构建在插件注册任务时,急切创建的任务更少。例如,使用 `TaskContainer#getByName(String, Closure)` 的逻辑应该转换为 `TaskContainer#named(String, Action)`。这还包括通过DSL块进行任务配置

  3. 将任务创建迁移到 `register(…​)`。
    此时,您应该将任何任务创建(使用 `create(…​)` 或类似方法)更改为使用 `register`。

进行这些更改后,您应该会看到在配置时急切创建的任务数量有所改善。

迁移故障排除

  • 哪些任务正在被实例化? 使用 Build Scan 按以下步骤进行故障排除

    1. 使用 `--scan` 标志执行Gradle命令。

    2. 导航到配置性能选项卡

      taskConfigurationAvoidance navigate to performance
    3. 所有所需信息将显示出来

      taskConfigurationAvoidance performance annotated
      1. 每个任务被创建或未创建时的总任务数。

        • “立即创建”表示使用急切任务API创建的任务。

        • “配置期间创建”表示使用配置避免API创建的任务,但通过 `TaskProvider#get()` 显式实例化或使用急切任务查询API隐式实例化。

        • “立即创建”和“配置期间创建”都被认为是“不良”数字,应尽可能最小化。

        • “任务执行期间创建”表示在任务图创建 之后 创建的任务。此时创建的任何任务都不会作为图的一部分执行。理想情况下,此数字应为零。

        • “任务图计算期间创建”表示在构建执行任务图时创建的任务。理想情况下,此数字应等于执行任务的数量。

        • “未创建”表示在此构建会话中避免的任务。理想情况下,此数字应尽可能大。

      2. 下一节将帮助回答任务在哪里被实现的问题。对于每个脚本、插件或生命周期回调,最后一列表示立即创建或在配置期间创建的任务。理想情况下,此列应为空。

      3. 专注于某个脚本、插件或生命周期回调将显示已创建任务的详细信息。

迁移陷阱

  • 警惕隐藏的急切任务实例化。 有多种方法可以急切地配置任务。
    例如,使用任务名称和DSL块配置任务将导致任务(在使用Groovy DSL时)立即被创建

    // Given a task lazily created with
    tasks.register("someTask")
    
    // Some time later, the task is configured using a DSL block
    someTask {
        // This causes the task to be created and this configuration to be executed immediately
    }

    改为使用 `named()` 方法获取任务的引用并进行配置

    tasks.named("someTask") {
        // ...
        // Beware of the pitfalls here
    }

    同样,Gradle具有语法糖,允许通过名称引用任务而无需显式查询方法。这也会导致任务立即被创建

    tasks.register("someTask")
    
    // Sometime later, an eager task is configured like
    task anEagerTask {
        // The following will cause "someTask" to be looked up and immediately created
        dependsOn someTask
    }

    有几种方法可以避免这种过早创建

    • 使用 `TaskProvider` 变量。 当任务在同一个构建脚本中多次引用时很有用。

      val someTask by tasks.registering
      
      task("anEagerTask") {
          dependsOn(someTask)
      }
      def someTask = tasks.register("someTask")
      
      task anEagerTask {
          dependsOn someTask
      }
    • 将消费者任务迁移到新的API。

      tasks.register("someTask")
      
      tasks.register("anEagerTask") {
          dependsOn someTask
      }
    • 惰性查找任务。 当任务不是由同一个插件创建时很有用。

      tasks.register("someTask")
      
      task("anEagerTask") {
          dependsOn(tasks.named("someTask"))
      }
      tasks.register("someTask")
      
      task anEagerTask {
          dependsOn tasks.named("someTask")
      }

要使用的惰性API

API 注意

返回一个 `TaskProvider` 而不是一个 `Task`。

返回一个 `TaskProvider` 而不是一个 `Task`。

可以使用。如果链式调用 `withType().getByName()`,请改用 `TaskCollection.named()`。

返回 `void`,因此不能链式调用。

要避免的急切API

API 注意

task myTask(type: MyTask) {}

请勿使用此简写符号。请改用 `register()`。

请改用 `register()`。

请勿使用。

请勿使用。

避免调用此方法。未来的行为可能会改变。

请改用 `named()`。

请改用 `named()`。

请改用 `DomainObjectCollection.configureEach()`。

如果您基于名称进行匹配,请改用 `named()`,它将是惰性的。 `matching()` 需要创建所有任务,因此请尝试通过限制任务类型来限制影响,例如 `withType().matching()`。

请改用 `named()`。

请改用 `withType().configureEach()`。

请改用 `configureEach()`。

请改用 `configureEach()`。

避免调用此方法。在大多数情况下,`matching()` 和 `configureEach()` 更适合。

请勿使用。`named()` 是最接近的等效项,但如果任务不存在,则会失败。

`iterator()` 或对 `Task` 集合的隐式迭代

避免这样做,因为它需要创建和配置所有任务。

remove()

避免调用此方法。未来的行为可能会改变。