约定插件是Gradle中不发布、无ID、仅限当前构建复用的插件,通过buildSrc目录自动编译注入,用于统一多模块配置(如Java版本、依赖约束),支持按模块条件启用,避免subprojects闭包式配置的强制继承与调试困难问题。

什么是约定插件(Convention Plugin)
约定插件是 Gradle 中一种“不暴露 ID、不发布、仅在当前构建内复用”的插件形式,本质是一个放在 buildSrc 或 gradle/convention 目录下的可编译 Gradle 插件。它不是用来被其他项目引用的,而是为了解决“多个子模块写重复 build.gradle 配置”这个具体问题——比如统一 Java 版本、测试框架、源码编码、依赖版本约束等。
怎么把约定插件放到 buildSrc 里并让所有子模块用上
关键不是“怎么写”,而是“怎么让 Gradle 找到并自动应用”。buildSrc 是 Gradle 的魔法目录:只要存在,Gradle 就会在配置阶段自动编译它,并将其 classpath 注入整个构建上下文。
-
buildSrc必须是根项目下的**独立目录**,不能叫buildsrc或build-src - 推荐结构:
buildSrc/src/main/kotlin/JavaLibraryConventionPlugin.kt(Kotlin DSL)或.groovy(Groovy DSL) - 插件类必须实现
Plugin,并在apply方法里做配置(如project.plugins.apply("java-library")、设置sourceCompatibility) - 不需要
pluginManagement块注册,也不需要id—— 它通过类名直接引用
在任意子模块的 build.gradle.kts 中,只需写一行:
plugins { id("com.example.java-library-convention") }
注意:这里的 ID 是你在插件类里用 PluginId 注解或 pluginBundle 配置的字符串,不是文件名。如果没配,就用全限定类名,比如 plugins { id("org.myproject.JavaLibraryConventionPlugin") }。
为什么不用 subprojects 而要用约定插件
直接在根 build.gradle.kts 里写 subprojects 看似简单,但会带来三个隐性问题:
- 所有子模块强制继承同一套逻辑,哪怕某个模块是 Kotlin-only 或 Android 库,也得硬套 Java 配置
- 无法按需启用:你不能说“只对
:service模块启用代码检查,:web不启” - 调试困难:
subprojects是闭包式配置,堆叠多层后,哪个配置覆盖了哪个,很难 trace
而约定插件天然支持条件分支和模块粒度控制。例如,在插件内部可以判断 project.name.startsWith("service-") 再决定是否添加某项依赖;也可以让不同模块 apply 不同插件组合,比如:
plugins {
id("com.example.java-library-convention")
id("com.example.test-convention") apply false // 按需开启
}
常见失败点:IDEA 同步失败或插件找不到
这不是插件写错了,而是构建上下文没搭对。最常踩的坑有三个:
-
buildSrc下没有build.gradle.kts(或build.gradle),导致 Gradle 不编译它——必须有,哪怕内容只是plugins { kotlin("jvm") version "1.9.20" } - 插件类用了
@Suppress("unused")或 IDE 自动删了未引用的 import,结果编译失败,但错误日志藏在buildSrc/build/reports/里,容易忽略 - 在子模块中写了
plugins { id("xxx") version "1.0" }—— 约定插件**没有版本号概念**,加 version 会导致 Gradle 去远端仓库找,必然报Plugin was not found
真正起作用的是编译后的类路径,不是 Maven 坐标。所以只要 buildSrc 编译成功,且子模块中 id 字符串匹配,就能用。
复杂点在于插件之间可能有隐式依赖关系——比如 Java 约定插件假设 Kotlin 插件已经 applied,但没声明 dependsOn。这种耦合不会报错,但会导致某些模块行为不一致。建议每个约定插件保持职责单一,并在文档里写清前提条件。











