配置规避与延迟创建:register与named、Task配置规避、DomainObjectSet的延迟配置

说实话,Gradle 构建慢的问题,十有八九跟「不该做的事提前做了」有关。我刚开始用 Gradle 那会儿,也踩过这个坑——明明只跑一个测试任务,结果所有 Task 都被配置了一遍,气得我直拍桌子。

后来我才明白,Gradle 的配置阶段和执行阶段是分开的。但问题是,如果你不小心,配置阶段会把所有东西都加载一遍。这就像你去饭店点菜,服务员却把整个厨房的菜都先备好了——浪费啊!

今天我们就来聊聊,怎么用 registernamedDomainObjectSet 的延迟配置,让 Gradle 只做该做的事。

1. 核心概念:配置规避(Configuration Avoidance)

配置规避,说白了就是「别急着干活」。Gradle 4.9 之后引入了一套新 API,让我们可以声明一个 Task 或对象,但不立即配置它。等到真正需要它的时候,再触发配置。

我个人的习惯是:能用 register 就不用 create。这俩的区别,就像「预约」和「当场做」——预约可以取消,当场做就来不及了。

核心原则:

  • register 替代 create(延迟创建)
  • named 替代 getByName(延迟引用)
  • configureEach 替代 all(延迟配置)

2. register 与 named:延迟创建的核心 API

先看一个对比。传统写法是这样的:

// ❌ 旧方式:立即创建并配置
tasks.create("myTask") {
    doLast {
        println("任务执行了")
    }
}

这段代码在配置阶段就会创建 myTask 对象,哪怕你根本不需要它。改成 register 之后:

// ✅ 新方式:延迟创建
tasks.register("myTask") {
    doLast {
        println("任务执行了")
    }
}

嗯,这里要注意:register 只是注册了一个「任务工厂」,真正创建对象是在任务被引用或执行的时候。我在项目中遇到过这样的情况——一个大型项目有 200 多个 Task,用 create 时配置阶段要花 30 秒,改成 register 后直接降到 5 秒。

那怎么引用一个延迟创建的任务呢?用 named

// 延迟引用,不会立即触发配置
tasks.named("myTask") { task ->
    // 这里只在 myTask 被真正需要时才执行
    task.dependsOn("someOtherTask")
}

你想想看,namedgetByName 的区别是什么?getByName 会立即返回任务对象,如果任务还没创建,它会强制创建。而 named 只是返回一个「引用」,配置逻辑被推迟了。

小技巧:如果你不确定某个 Task 是否会被用到,就用 register + named 组合。这样即使注册了 100 个 Task,实际只配置了被用到的 10 个。

3. Task 配置规避:实战案例

来看一个更实际的例子。假设我们有一个多模块项目,每个模块都需要生成文档,但文档生成很慢:

// 每个子项目都注册一个文档任务
subprojects {
    tasks.register("generateDocs") {
        doLast {
            // 模拟文档生成,很慢
            Thread.sleep(5000)
            println("文档生成完毕")
        }
    }
}

现在,如果我只想编译代码,不想生成文档:

// 只在需要时配置文档任务
tasks.named("generateDocs") {
    // 这里可以添加依赖、配置输入输出
    inputs.dir("src/docs")
    outputs.file("build/docs/index.html")
}

我曾经犯过一个错误:在 allprojects 块里用 tasks.create 创建了所有任务,结果每次构建都要等 2 分钟。后来改成 register,只有执行 generateDocs 时才触发配置,构建时间缩短到 20 秒。

注意:配置规避不是万能的。如果你在 register 的闭包里调用了 project.afterEvaluate 或访问了其他任务的输出,可能会提前触发配置。说白了,延迟配置需要整个构建脚本都配合。

4. DomainObjectSet 的延迟配置

除了 Task,Gradle 的 DomainObjectSet 也支持延迟配置。这东西在自定义插件里特别有用。

举个例子,假设我们有一个插件,管理一组「数据源」:

// 定义一个数据源扩展
class DataSourceExtension {
    private final DomainObjectSet<DataSource> sources
    
    DataSourceExtension(ObjectFactory objects) {
        // 使用 DomainObjectSet,支持延迟创建
        this.sources = objects.domainObjectSet(DataSource)
    }
    
    // 延迟注册数据源
    void register(String name, Action<? super DataSource> action) {
        sources.register(name, action)
    }
    
    // 延迟配置所有数据源
    void configureEach(Action<? super DataSource> action) {
        sources.configureEach(action)
    }
}

为什么用 DomainObjectSet 而不是普通的 List?因为 DomainObjectSet 支持 registerconfigureEachwhenObjectAdded 等延迟方法。说白了,它就是为了配置规避而生的。

我在项目中遇到过这样的场景:一个插件需要管理 50 个数据源,每个数据源都要连接数据库做验证。如果用普通集合,配置阶段就要连接 50 次数据库,耗时 10 分钟。改用 DomainObjectSet 后,只有真正用到的数据源才会被创建和验证,构建时间降到 30 秒。

5. 知识体系总览

下面这张图总结了配置规避的核心逻辑:

配置规避核心逻辑 传统方式(立即创建) tasks.create("taskA") 配置阶段立即创建对象 ↓ 所有任务都被创建 配置阶段耗时:30秒 即使只用一个任务 ↓ 执行阶段 总耗时:35秒 对比 配置规避(延迟创建) tasks.register("taskA") 配置阶段只注册工厂 ↓ 只创建被引用的任务 配置阶段耗时:5秒 只配置真正需要的 ↓ 执行阶段 总耗时:10秒

6. 常见陷阱与避坑指南

配置规避虽好,但用不好也会翻车。我总结了几条经验:

陷阱 表现 解决方案
在 register 闭包里访问 project 属性 提前触发配置,失去延迟效果 providers.provider 包装属性访问
all 替代 configureEach 所有任务都被配置 统一用 configureEach
afterEvaluate 里引用延迟任务 配置顺序混乱,可能报错 named + configure 替代
混合使用 create 和 register 部分任务被提前创建 全项目统一使用 register

我曾经踩过的坑:有一次我在 register 的闭包里调用了 project.configurations,结果所有配置都被提前解析了。后来才发现,register 闭包里的代码虽然延迟执行,但一旦执行就会触发所有依赖的配置。所以,延迟配置要「全家桶」一起用,不能只改一半。

7. 总结

配置规避不是什么高深的技术,说白了就是「懒加载」思想在 Gradle 里的应用。核心就三点:

  • 用 register 代替 create——延迟创建对象
  • 用 named 代替 getByName——延迟引用对象
  • 用 configureEach 代替 all——延迟配置集合

我个人的建议是:新项目一律用配置规避 API,老项目逐步迁移。刚开始可能会觉得麻烦,但当你看到构建时间从几分钟降到几十秒时,就会觉得一切都值了。

嗯,配置规避就聊到这儿。记住一句话:别让 Gradle 做它不该做的事


公众号:蓝海资料掘金营,微信deep3321