配置规避与延迟创建:register与named、Task配置规避、DomainObjectSet的延迟配置
说实话,Gradle 构建慢的问题,十有八九跟「不该做的事提前做了」有关。我刚开始用 Gradle 那会儿,也踩过这个坑——明明只跑一个测试任务,结果所有 Task 都被配置了一遍,气得我直拍桌子。
后来我才明白,Gradle 的配置阶段和执行阶段是分开的。但问题是,如果你不小心,配置阶段会把所有东西都加载一遍。这就像你去饭店点菜,服务员却把整个厨房的菜都先备好了——浪费啊!
今天我们就来聊聊,怎么用 register、named 和 DomainObjectSet 的延迟配置,让 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")
}
你想想看,named 和 getByName 的区别是什么?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 支持 register、configureEach、whenObjectAdded 等延迟方法。说白了,它就是为了配置规避而生的。
我在项目中遇到过这样的场景:一个插件需要管理 50 个数据源,每个数据源都要连接数据库做验证。如果用普通集合,配置阶段就要连接 50 次数据库,耗时 10 分钟。改用 DomainObjectSet 后,只有真正用到的数据源才会被创建和验证,构建时间降到 30 秒。
5. 知识体系总览
下面这张图总结了配置规避的核心逻辑:
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