本章介绍了处理任务时的"避免配置",并解释了一些有关迁移构建以有效使用避免配置API的准则. 这里描述的API与现有的API共存,在几个主要版本中,这些API将被我们的常规弃用过程所取代. 从Gradle 5.1开始,我们建议每当自定义插件创建任务时都使用避免配置API.

How does the configuration avoidance API work?

简而言之,API允许构建避免在Gradle的配置阶段(这些任务将永远不会执行)期间创建和配置任务的成本. 例如,在运行编译任务时,将不执行其他无关的任务,例如代码质量,测试和发布任务,因此无需花费任何时间来创建和配置这些任务. 如果在构建过程中不需要任务,则避免配置API会避免配置任务,这可能会对总配置时间产生重大影响.

为了避免创建和配置任务,我们说任务是"已注册"但未创建. 当任务处于这种状态时,它是构建已知的,可以对其进行配置,并且可以传递对其的引用,但是实际上尚未创建任务对象本身,并且没有执行任何配置操作. 它将保持此状态,直到构建中需要实例化的任务对象为止(例如,如果该任务是在命令行上执行的,或者该任务是在命令行上执行的任务的依赖项). 如果不再需要任务对象,则任务将保持注册状态,从而避免了创建和配置任务的成本.

在Gradle中,您可以使用TaskContainer.register(java.lang.String)来注册任务. 此方法有多种变体,允许提供任务类型和/或用于修改任务配置的操作. register(…​)方法没有返回任务实例,而是返回TaskProvider ,它是对任务的引用,该引用可以在可能使用普通任务对象的许多地方使用(例如在创建任务依赖项时).

Guidelines

How do I defer task creation?

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

较旧的Gradle版本仅支持create(…​) API. 当调用create(…​) API时,它会急于创建和配置任务,因此应避免使用.

仅使用register(…​)可能不足以完全避免所有任务配置. 您可能需要更改其他按名称或类型配置任务的代码,如以下各节所述.

How do I defer task configuration?

DomainObjectCollection.all(org.gradle.api.Action)DomainObjectCollection.withType(java.lang.Class,org.gradle.api.Action)之类的Eager API将立即创建并配置任何已注册的任务. 要推迟任务配置,您将需要迁移到等效的配置避免API. 请参阅下以确定替代方法.

How do I reference a task without creating/configuring it?

除了引用任务对象,您还可以通过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 .继续工作.

如果要按名称配置任务,则需要使用等效的配置避免措施. 请参阅下以确定替代方法.

How to get an instance of a Task?

如果仍然需要访问Task实例,则可以使用TaskCollection.named(java.lang.String)并调用Provider.get() . 这将导致任务的创建/配置,但是一切都应该与渴望的API一样工作.

Migration Guide

以下各节将介绍一些常规准则 ,这些准则在迁移构建逻辑以及我们建议遵循的步骤都应遵循. 我们还将介绍一些故障排除陷阱,以帮助您解决在迁移过程中可能遇到的一些问题.

General

  1. 在迁移过程中,请使用help任务作为基准. help任务是基准测试迁移过程的理想选择. 在仅使用配置避免API 的构建中构建扫描不会显示立即创建或在配置期间创建的任务,而只会创建实际执行的任务. 请注意所使用的构建扫描插件的版本 .

  2. 仅在配置操作中更改当前任务. 因为任务配置操作现在可以立即,稍后或永远不会运行,所以对当前任务以外的任何其他变量进行更改都可能导致构建中行为不确定. 考虑以下代码:

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

    执行gradle check任务应执行verificationTask ,但在此示例中,它不会执行. 这是因为之间的依赖关系verificationTaskcheck ,只有当发生verificationTask实现. 为避免出现此类问题,您必须仅修改与配置操作关联的任务. 其他任务应在自己的配置操作中进行修改. 该代码将变为:

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

    将来,Gradle会将这种反模式视为错误,并将产生异常.

  3. 最好是小的增量更改. 较小的更改更易于检查. 如果您破坏了构建逻辑,自上次成功验证以来,分析变更日志将更加容易.

  4. 确保建立用于验证构建逻辑的好的计划. 通常,一个简单的build任务调用就可以完成验证构建逻辑的技巧. 但是,某些构建可能需要其他验证-了解构建的行为并确保您有一个好的验证计划.

  5. 与自动测试相比,首选自动测试. 优良作法是使用TestKit为构建逻辑编写集成测试.

  6. 避免按名称引用任务. 在大多数情况下,按名称引用任务是一种脆弱的模式,应避免使用. 尽管任务名称在TaskProvider上可用,但应努力使用来自强类型模型的引用.

  7. 尽可能使用新的任务API. 急于实现某些任务可能会导致实现其他任务的级联. 使用TaskProvider有助于创建防止传递实现的间接访问.

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

Migration Steps

迁移过程的第一部分是遍历代码并手动迁移渴望的任务创建和配置,以使用配置回避API. 以下内容探讨了成功迁移的建议步骤. 在执行这些步骤时,请记住上面的准则 .

在插件中使用新的API将要求用户使用Gradle 4.9或更高版本. 插件作者应参考" 支持较早版本的Gradle"部分.

  1. 迁移会影响所有任务( tasks.all {} )或按类型影响子集的任务配置( tasks.withType(…​) {} ). 这将导致您的构建急切地减少由插件注册的任务.

  2. 迁移按名称配置的任务. 与上一点类似,这将导致您的构建急切地创建更少的由插件注册的任务. 例如,使用TaskContainer#getByName(String, Closure/Action)逻辑应转换为TaskContainer#named(String).configure(Closure/Action) . 这也包括通过DSL块进行的任务配置 .

  3. 将任务创建迁移到register(…​) . 此时,您应该更改正在创建任务的任何位置,以注册这些任务.

对于上述所有步骤,请注意延迟配置周围常见陷阱 .

进行了这些更改之后,您应该看到配置时急切创建的任务数量有所改善. 使用构建扫描来了解仍在急切创建哪些任务以及发生的位置.

Troubleshooting

  • 正在实现什么任务? 随着我们不断开发该功能,将提供更多报告和故障排除信息来回答此问题. 同时, 构建扫描是回答此问题的最佳方法 . 跟着这些步骤:

    1. 创建一个构建扫描 . 使用--scan标志执行Gradle命令.

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

      taskConfigurationAvoidance navigate to performance
      图1.导航到构建扫描中的"配置性能"选项卡
      1. 从左侧菜单导航到性能卡.

      2. 从性能卡顶部导航到"配置"选项卡.

    3. 将显示所有需要的信息.

      taskConfigurationAvoidance performance annotated
      图2.构建扫描中带有注释的"配置性能"选项卡
      1. 不论是否创建每个任务,总任务总数.

        • "立即创建"表示使用急切任务API创建的任务.

        • "在配置过程中创建"表示使用配置回避API创建的任务,但已明确地(通过TaskProvider#get() )或使用急切的任务查询API隐式实现的任务.

        • "立即创建"和"在配置过程中创建"这两个数字均被视为"不良"数字,应尽量减少这些数字.

        • "在任务图计算期间创建"表示构建执行任务图时创建的任务. 理想情况下,此数目应等于已执行任务的数目.

        • "未创建"表示在此构建会话中避免的任务.

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

      3. 着重于脚本,插件或生命周期回调将显示已创建任务的分解.

Pitfalls

  • 当心隐藏的渴望任务的实现. 可以通过多种方式来快速配置任务. 例如,使用任务名称和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").configure {
        // ...
        // 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变量. 在同一构建脚本中多次引用任务时很有用.

      def someTask = tasks.register("someTask")
      
      task anEagerTask {
          dependsOn someTask
      }
      val someTask by tasks.registering
      
      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"))
      }
  • 在版本1.15之前,构建扫描插件buildScanPublishPrevious一直很渴望. Upgrade the build scan plugin in your build to use the latest version.

Supporting older versions of Gradle

本节介绍了两种方法,可以使您的插件与Gradle的较早版本向后兼容,以便您必须与4.9之前的Gradle保持兼容. 从Gradle 4.9开始,大多数新的API方法都可用.

尽管向后兼容性对用户有利,但我们仍建议您及时升级到较新的Gradle版本. 这将减轻您的维护负担.

保持兼容性的第一种方法是根据Gradle 4.9 API编译插件,并使用Groovy有条件地调用正确的API( 示例 ).

第二种方法是使用Java反射来解决API在编译期间不可用的事实( 示例 ).

强烈建议使用TestKit和Gradle的多个版本进行跨版本测试.

Existing vs New API overview

  • 在新的API中,采用org.gradle.api.Action方法涵盖了采用groovy.lang.Closure方法.

  • 将来可能会基于用户反馈添加更多便捷方法.

  • 某些旧的API方法可能永远无法在新的API中直接替换.

  • 在通过配置避免方法注册的配置操作中访问某些API时,可能会受到限制.

新旧API Description

代替: task myTask(type: MyTask) {}

没有使用新API的速记Groovy DSL.

Use: tasks.register("myTask", MyTask) {}

使用以下替代方法之一.

用途:无直接等效物.

使用以下替代方法之一.

用途:无直接等效物.

这将返回TaskProvider而不是Task .

这将返回TaskProvider而不是Task .

这将返回TaskProvider而不是Task .

这将返回TaskProvider而不是Task .

这将返回TaskProvider而不是Task .

这将返回TaskProvider而不是Task .

这将返回TaskProvider而不是Task .

Use: named(java.lang.String).configure(Action)

从另一个项目访问任务需要对项目评估进行特定排序.

用途:无直接等效物.

named(String)是最接近的等效项,但是如果任务不存在,它将失败. 使用findByName(String)将导致创建/配置使用新API注册的任务.

使用:无直接等效.

See getByPath(String) above.

用途:无直接等效物.

可以使用此方法,因为它不需要立即创建任务.

Use: OK

代替: withType(java.lang.Class).getByName(java.lang.String)

这将返回TaskProvider而不是Task .

Use: named(java.lang.String, java.lang.Class)

这将返回void ,因此无法进行链接.

Use: withType(java.lang.Class).configureEach(org.gradle.api.Action)

这将返回void ,因此无法进行链接.

这将返回void ,因此无法进行链接.

This returns void, so it cannot be chained.

避免调用此方法. 大多数情况下, matching(Spec)configureEach(Action)更合适.

使用: 确定 ,有问题.

可以使用此方法,因为它不需要立即创建任务.

Use: OK

Avoid calling this directly as it’s a Groovy convenience method. The alternative returns a TaskProvider instead of a Task.

代替: iterator()Task集合上的隐式迭代

避免这样做,因为它需要创建和配置所有任务. 请参阅上面的findAll(Closure) .

使用: 确定 ,有问题.

代替: remove(org.gradle.api.Task)

避免调用此. 将来使用新API进行remove的行为可能会更改.

使用: 确定 ,有问题.

避免调用此. 将来可能会replace为新API的行为.

使用: 确定 ,有问题.

避免调用此. 将来可能会replace为新API的行为.

使用: 确定 ,有问题.