9、自定义扩展:ExtensionContainer、创建DSL扩展、嵌套扩展、延迟配置与Provider、扩展与任务联动
各位同学,今天我们来聊聊 Gradle 插件开发里最核心、也最有趣的部分——自定义扩展。
说实话,我刚开始学 Gradle 插件时,最头疼的就是 DSL 那套东西。看着 build.gradle.kts 里那些花里胡哨的配置块,心想这玩意儿到底是怎么跟底层代码连起来的?后来踩了不少坑,才慢慢摸清楚门道。
今天我就把这块内容掰开揉碎了讲给你听。你学完之后,不仅能看懂别人的 DSL,还能自己写一套出来。
9.1 ExtensionContainer 是什么?
先问一个问题:你在 build.gradle.kts 里写的 android { ... } 或者 dependencies { ... },Gradle 是怎么知道这些配置块对应哪个对象的?
答案就是 ExtensionContainer。
ExtensionContainer 是 Gradle 项目的一个容器,专门用来管理所有扩展对象。每个 Project 都有一个 ExtensionContainer,你可以通过 project.extensions 拿到它。
我个人习惯把 ExtensionContainer 理解成一个「注册中心」。你写的每一个 DSL 块,本质上都是向这个中心注册了一个扩展对象,然后 Gradle 在解析脚本时,会根据名称找到对应的对象并执行配置闭包。
核心方法就三个:
create(name, type, ...args)— 创建并注册一个扩展findByName(name)— 按名称查找扩展,找不到返回 nullgetByName(name)— 按名称获取扩展,找不到抛异常
举个例子,你在插件里这样写:
// 插件 apply 时调用
override fun apply(project: Project) {
project.extensions.create("myConfig", MyExtension::class.java)
}
然后在 build.gradle.kts 里就能这样用:
myConfig {
name = "Hello"
version = 1
}
是不是很简单?说白了,就是注册一个名字,绑定一个类,剩下的交给 Gradle 去解析。
9.2 创建 DSL 扩展
刚才的例子是最基础的用法。但实际项目中,我们往往需要更复杂的 DSL 结构。比如你希望用户这样写:
myConfig {
name = "App"
version = 2
enableLog = true
}
对应的扩展类长这样:
open class MyExtension {
var name: String = ""
var version: Int = 0
var enableLog: Boolean = false
}
这里有个小细节:类必须用 open 修饰。为什么?因为 Gradle 在创建扩展对象时,可能会用动态代理去包装它。如果你用 final 类,Gradle 会直接报错。
避坑指南:我曾经在项目里忘了加 open,结果编译时一切正常,运行到配置阶段直接崩了。排查了半天才发现是类没开放继承。所以记住,扩展类一定要 open。
另外,如果你希望 DSL 支持 lambda 配置块(也就是花括号那种写法),扩展类需要实现一个带 Action<T> 参数的方法。不过大多数情况下,直接用属性赋值就够了。
9.3 嵌套扩展
真实项目里,配置往往不是扁平的。比如 Android 的 defaultConfig { ... } 里面还有 buildConfigField、ndk { ... } 这种嵌套结构。
嵌套扩展的实现方式有两种:
- 内部类方式:在扩展类里定义内部类,通过方法返回子扩展对象
- 独立类方式:每个嵌套块对应一个独立的类,通过容器注册
我个人更推荐独立类方式,因为代码更清晰,也方便单元测试。
来看一个例子:
// 主扩展
open class MyExtension {
val server = ServerConfig()
fun server(action: Action<ServerConfig>) {
action.execute(server)
}
}
// 嵌套扩展
open class ServerConfig {
var url: String = ""
var port: Int = 8080
var timeout: Long = 5000
}
用户使用时:
myConfig {
server {
url = "https://api.example.com"
port = 443
}
}
注意看 server { ... } 这个写法。它其实是一个方法调用,参数是一个 Action 闭包。闭包内部执行的 url = ... 实际上是在配置 ServerConfig 对象的属性。
注意:嵌套扩展的类也必须是 open。另外,如果嵌套层数超过三层,建议重新考虑设计——太深的 DSL 会让用户崩溃的。
9.4 延迟配置与 Provider
这是很多新手容易忽略的点。Gradle 的构建过程分为三个阶段:初始化 → 配置 → 执行。
如果你在配置阶段就读取了某个文件或计算了某个值,但那个值其实要到执行阶段才确定,那就会出问题。
举个例子:
// 错误做法
val buildTime = System.currentTimeMillis()
myConfig {
timestamp = buildTime // 这个值在配置阶段就固定了
}
为什么说这是错的?因为 Gradle 的配置阶段可能因为缓存、增量构建等原因被跳过。如果你把时间戳写死在配置阶段,那每次构建的时间戳都一样,缓存就失效了。
正确的做法是用 Provider:
// 扩展类里用 Provider
open class MyExtension {
val timestamp: Provider<Long> = project.provider {
System.currentTimeMillis()
}
}
Provider 是一个延迟计算的容器。它只在被访问时才执行内部的 lambda,而且 Gradle 会帮你管理它的依赖关系。如果某个输入没变,Provider 甚至可以返回缓存的结果。
核心原则:能用 Provider 的地方,就别用普通值。尤其是在扩展属性里,尽量用 Property<T> 或 Provider<T>。
Gradle 提供了几种常用的延迟类型:
| 类型 | 说明 | 使用场景 |
|---|---|---|
| Property<T> | 可读写的延迟属性 | 用户配置的值,比如版本号 |
| Provider<T> | 只读的延迟计算 | 从其他 Provider 转换而来 |
| ListProperty<T> | 可读写的列表 | 多个输入文件列表 |
| SetProperty<T> | 可读写的集合 | 去重的配置项 |
| MapProperty<K, V> | 可读写的映射 | 键值对配置 |
你想想看,如果不用 Provider,你在配置阶段就计算了某个值,但执行阶段这个值依赖的文件变了,那你的计算结果就是错的。Provider 就是为了解决这种「配置时不知道,执行时才确定」的问题。
9.5 扩展与任务联动
扩展写好了,Provider 也用了,但扩展里的配置怎么传递给任务呢?
这里有个关键点:任务是在配置阶段创建的,但任务的输入输出是在执行阶段才使用的。
所以,你不能在创建任务时直接读取扩展的值,而应该把扩展的 Provider 传递给任务。
来看一个完整的例子:
// 插件类
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
// 1. 注册扩展
val extension = project.extensions.create("myConfig", MyExtension::class.java)
// 2. 注册任务
project.tasks.register("myTask", MyTask::class.java) { task ->
// 3. 将扩展的 Provider 连接到任务的输入
task.message.set(extension.message)
task.outputFile.set(extension.outputFile)
}
}
}
// 扩展类
open class MyExtension {
val message: Property<String> = ObjectFactory.property(String::class.java)
val outputFile: Property<File> = ObjectFactory.property(File::class.java)
}
// 任务类
abstract class MyTask : DefaultTask() {
@get:Input
abstract val message: Property<String>
@get:OutputFile
abstract val outputFile: Property<File>
@TaskAction
fun doWork() {
val msg = message.get()
val file = outputFile.get()
file.writeText(msg)
}
}
注意看任务注册时的 lambda:task.message.set(extension.message)。这里 set 的是一个 Provider,而不是具体的值。Gradle 会在执行任务时,自动解析这个 Provider 的值。
个人经验:我曾经在项目里直接传了字符串值,结果每次改配置都要 clean 才能生效。后来改成 Provider 传递,增量构建就正常了。所以记住,任务输入输出一定要用 Provider 或 Property。
另外,如果你希望任务在配置阶段就依赖扩展的值(比如根据扩展的值决定是否注册任务),那可以用 extension.message.orNull 来判断。但这种情况比较少,大多数时候还是延迟到执行阶段再取值。
9.6 知识体系总览
说了这么多,我们来画一张图,把整个知识体系串起来。
这张图把今天讲的内容都串起来了。你看,ExtensionContainer 是中心,它负责管理所有扩展。扩展可以简单(直接属性),也可以嵌套(内部再分块)。配置值尽量用 Provider 延迟计算,最后通过任务注册时的 set 方法联动到任务输入输出。
嗯,到这里,自定义扩展的核心内容就讲完了。你可能会问:这些知识点在实际项目中怎么用?其实你去看 Android 官方的 Gradle 插件源码,里面全是这套模式。理解了今天的内容,你就能看懂官方插件是怎么设计的,甚至能自己写一个类似 android { ... } 的 DSL。
记住一句话:扩展是配置的入口,Provider 是延迟的保障,任务是执行的终点。这三者配合好了,你的插件就能既灵活又高效。