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 只做两件事——注册扩展、应用约定。具体的逻辑拆分到单独的函数或类里。

小技巧:apply 方法里不要写太多业务逻辑。把它当成一个「入口」,真正的实现放到扩展或 task 里。

插件 ID:你的插件的身份证

插件 ID 是啥?就是别人用你插件时写在 plugins { } 块里的那个字符串。比如 id("com.example.my-plugin")

命名规则其实挺讲究的。我建议用反向域名的方式,比如 com.yourcompany.youplugin。这样能避免冲突。你想想看,万一有人也写了个 my-plugin,那不就撞车了?

注册插件 ID 有两种方式:

  1. 在 build.gradle.kts 里直接声明(适用于自用插件)
  2. 通过 gradlePlugin 块声明(适用于发布到插件门户)
// 方式一:直接声明
plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        register("myPlugin") {
            id = "com.example.my-plugin"
            implementationClass = "com.example.MyPlugin"
        }
    }
}

我曾经踩过一个坑:插件 ID 里用了大写字母。结果在某个 Linux CI 机器上死活找不到插件。后来才发现,Gradle 插件 ID 是大小写敏感的,但文件系统不敏感……嗯,从那以后我全用小写加横线。

注意:插件 ID 建议只用小写字母、数字和横线。不要用点号以外的特殊字符。发布到 Gradle Plugin Portal 时,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 的魅力。我个人习惯把扩展设计成扁平结构,不要嵌套太深。嵌套多了,用户写起来累,你解析起来也累。

核心要点:扩展的名字(比如上面的 "myPlugin")就是用户在 DSL 块里用的名字。建议和插件 ID 的后半部分保持一致,方便记忆。

约定:让用户少写代码

约定(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 还是默认值,用户配置根本没生效。调试了整整一个下午……

避坑指南:如果你需要在 task 里使用扩展的值,建议在 task 的 doFirstdoLast 里读取,而不是在配置阶段。或者用 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 里读取扩展的值。这就是自定义插件的核心套路。

知识体系总览

下面这张图帮你理清自定义插件的整体结构:

自定义插件核心结构 Plugin<Project> apply(project) 注册扩展 extensions.create(...) 约定与 Task tasks.register(...) 用户 DSL 配置 myPlugin { ... } Task 执行 doFirst / doLast 构建输出 / 产物

从这张图能看出来,自定义插件的核心就三个环节:插件入口 → 注册扩展 → 约定与 Task。用户通过 DSL 配置扩展,插件在 Task 执行时读取这些配置,最终输出结果。

我个人觉得,写插件最爽的一点是:你是在为别人创造「语法」。好的插件能让 build.gradle.kts 读起来像自然语言一样流畅。这需要你对 DSL 设计有感觉,多写几次就熟练了。

总结一下:
  • 插件接口:实现 Plugin<Project>,在 apply 里做入口
  • 插件 ID:反向域名命名,全小写加横线
  • 扩展:用 extensions.create 注册,让用户可配置
  • 约定:提供默认值,用 afterEvaluate 确保配置就绪

好了,这一章的内容就到这里。记住:插件开发不是魔法,它只是把重复劳动封装起来的工程手段。你写得越多,就越能体会到它的威力。


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