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

writing tasks 4

任务配置避免 API

配置避免 API 避免配置构建中不会使用的任务,这可以显著影响总配置时间。

例如,当运行 compile 任务(应用了 java 插件)时,其他不相关的任务(例如 cleantestjavadocs)将不会被执行。

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

当任务被注册时,构建系统就知道了它的存在。它可以被配置,并且可以传递对其的引用,但任务对象本身尚未被创建,其操作也尚未被执行。注册的任务将保持这种状态,直到构建中的某些东西需要实例化的任务对象。如果永远不需要任务对象,则任务将保持注册状态,并且可以避免创建和配置任务的成本。

在 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 以使显式依赖项继续工作。

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

引用任务实例

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

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

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

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

当需要建立任务关系时(即 dependsOnfinalizedBymustRunAftershouldRunAfter),可以在软关系和强关系之间进行区分。它们在配置阶段对任务创建的影响不同

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

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

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

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

迁移指南

以下各节将介绍迁移构建逻辑时需要遵循的一些通用指南。我们还提供了一些建议的步骤,以及问题排查常见陷阱

迁移指南

  1. 在迁移期间,使用 help 任务作为基准。
    help 任务是将您的迁移过程作为基准的完美候选对象。在使用配置避免 API 的构建中,构建扫描显示在配置期间未创建任何任务,并且仅创建了执行的任务。

  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,但在此示例中,它不会。这是因为 verificationTaskcheck 之间的依赖关系仅在 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 可能会被禁止。
    例如,在配置使用新 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()

避免调用此方法。该行为将来可能会更改。