16、自定义插件开发:插件接口、插件ID、插件扩展与约定
说实话,Gradle 里最让我着迷的就是自定义插件。
你想想看,一个项目里几十个模块,每个模块都要重复配置依赖、仓库、编译选项……写起来烦不烦?我当年在一个微服务项目里,光复制粘贴 build.gradle 就花了整整两天。后来我意识到——该写个插件了。
自定义插件,说白了就是把重复的逻辑封装起来。一次编写,到处复用。今天我就带你走一遍完整的流程:从插件接口到插件 ID,再到扩展和约定。
插件接口:一切从这里开始
Gradle 插件本质上就是一个实现了 Plugin<Project> 接口的类。这个接口只有一个方法:apply(Project target)。
嗯,就这么简单。你在这个方法里写什么,插件就做什么。
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
println("Hello from MyPlugin!")
}
}
我在项目中遇到过不少新手,一上来就写几百行的 apply 方法。其实没必要。我个人的习惯是:apply 只做两件事——注册扩展、应用约定。具体的逻辑拆分到单独的函数或类里。
插件 ID:你的插件的身份证
插件 ID 是啥?就是别人用你插件时写在 plugins { } 块里的那个字符串。比如 id("com.example.my-plugin")。
命名规则其实挺讲究的。我建议用反向域名的方式,比如 com.yourcompany.youplugin。这样能避免冲突。你想想看,万一有人也写了个 my-plugin,那不就撞车了?
注册插件 ID 有两种方式:
- 在 build.gradle.kts 里直接声明(适用于自用插件)
- 通过
gradlePlugin块声明(适用于发布到插件门户)
// 方式一:直接声明
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
register("myPlugin") {
id = "com.example.my-plugin"
implementationClass = "com.example.MyPlugin"
}
}
}
我曾经踩过一个坑:插件 ID 里用了大写字母。结果在某个 Linux CI 机器上死活找不到插件。后来才发现,Gradle 插件 ID 是大小写敏感的,但文件系统不敏感……嗯,从那以后我全用小写加横线。
插件扩展:让用户能配置你的插件
光有插件还不够。用户需要能调整行为,对吧?比如设置输出目录、开关某个功能。这时候就需要扩展(Extension)了。
扩展说白了就是一个数据类,用户可以在 build.gradle.kts 里用 DSL 的方式配置它。
// 定义扩展
open class MyPluginExtension {
var outputDir: String = "build/output"
var enableLogging: Boolean = false
}
// 在插件中注册
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(
"myPlugin",
MyPluginExtension::class.java
)
// 后续可以用 extension 读取配置
}
}
用户使用时是这样的:
myPlugin {
outputDir = "build/custom-output"
enableLogging = true
}
是不是很自然?这就是 Kotlin DSL 的魅力。我个人习惯把扩展设计成扁平结构,不要嵌套太深。嵌套多了,用户写起来累,你解析起来也累。
约定:让用户少写代码
约定(Convention)是 Gradle 插件里一个很妙的设计。它的思路是:你提供默认值,用户按需覆盖。
举个例子。我写过一个 Android 模块的插件,默认把输出目录设为 build/reports。大部分项目都直接用这个路径,只有少数需要改。那他们只需要在扩展里改一下就行。
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(
"myPlugin",
MyPluginExtension::class.java
)
// 约定:如果用户没设置,就用默认值
project.afterEvaluate {
val dir = extension.outputDir
println("输出目录:$dir")
}
}
}
这里有个细节:afterEvaluate。为什么要用它?因为用户在 build.gradle.kts 里写的配置,是在配置阶段执行的。如果你在 apply 里直接读 extension,那时候用户还没写配置呢。等到 afterEvaluate 回调时,所有配置都就绪了。
我曾经犯过一个错:在 apply 里直接创建 task 并依赖 extension 的值。结果 task 创建时 extension 还是默认值,用户配置根本没生效。调试了整整一个下午……
doFirst 或 doLast 里读取,而不是在配置阶段。或者用 afterEvaluate 确保配置已就绪。
完整示例:一个日志插件
光说不练假把式。我们来写一个完整的插件:它会在构建时输出一条日志,用户可以自定义日志级别和消息。
// 扩展
open class LogPluginExtension {
var message: String = "构建完成"
var level: String = "INFO" // INFO, WARN, ERROR
}
// 插件
class LogPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(
"logPlugin",
LogPluginExtension::class.java
)
project.tasks.register("printLog") {
doLast {
val msg = extension.message
val lvl = extension.level
when (lvl.uppercase()) {
"WARN" -> logger.warn(msg)
"ERROR" -> logger.error(msg)
else -> logger.lifecycle(msg)
}
}
}
}
}
用户使用:
plugins {
id("com.example.log-plugin")
}
logPlugin {
message = "所有模块编译通过!"
level = "INFO"
}
tasks.named("build") {
dependsOn("printLog")
}
你看,整个流程很清晰:定义扩展 → 注册扩展 → 在 task 里读取扩展的值。这就是自定义插件的核心套路。
知识体系总览
下面这张图帮你理清自定义插件的整体结构:
从这张图能看出来,自定义插件的核心就三个环节:插件入口 → 注册扩展 → 约定与 Task。用户通过 DSL 配置扩展,插件在 Task 执行时读取这些配置,最终输出结果。
我个人觉得,写插件最爽的一点是:你是在为别人创造「语法」。好的插件能让 build.gradle.kts 读起来像自然语言一样流畅。这需要你对 DSL 设计有感觉,多写几次就熟练了。
- 插件接口:实现
Plugin<Project>,在apply里做入口 - 插件 ID:反向域名命名,全小写加横线
- 扩展:用
extensions.create注册,让用户可配置 - 约定:提供默认值,用
afterEvaluate确保配置就绪
好了,这一章的内容就到这里。记住:插件开发不是魔法,它只是把重复劳动封装起来的工程手段。你写得越多,就越能体会到它的威力。