脚本插件是.gradle文件,通过apply from引入,仅作代码复制执行,无命名空间、不可发布、无版本管理;二进制插件须实现plugin接口、打包为jar、支持id声明、可复用、可发布、具备扩展和生命周期监听能力。

脚本插件(.gradle)和自定义二进制插件根本不是同一类东西
很多人一上来就混淆了“写在 build.gradle 里的逻辑”和“真正意义上的自定义插件”。apply from: 'common.gradle' 加载的是脚本插件,它只是把那段代码原样塞进当前构建脚本里执行,没有类加载隔离、没有版本号、不能发布到仓库,连 IDE 都没法跳转到它的定义位置。而一个真正的自定义插件(比如用 Kotlin 写的 MyBuildPlugin 类),必须实现 Plugin<project></project> 接口,打包成 jar,通过 plugins { id "com.example.mybuild" version "1.0" } 或 apply plugin: "com.example.mybuild" 引入——它有独立的类路径、可被多个项目复用、能声明扩展(extensions.create)、也能参与 Gradle 的依赖解析和插件约束机制。
build.gradle 里直接写逻辑 vs 插件里写 apply(Project) 方法
你在 build.gradle 里写 tasks.register("hello") { doLast { println "hi" } },这段代码会在配置阶段立即执行;但如果你把它搬到插件的 apply(Project project) 方法里,执行时机一样,区别在于作用域和可维护性:
- 脚本里写的任务/配置只对当前项目生效,复制到另一个模块就得改一遍路径、依赖名、条件判断
- 插件里的逻辑可以读取
project.extensions.findByType(MyExtension),让使用者通过 DSL 配置行为,比如mybuild { enabled = true; outputDir = "build/reports" } - 插件能监听生命周期事件,比如在
project.afterEvaluate后检查是否漏配了必要属性,而纯脚本很难做这种跨阶段校验 - Gradle 8.0+ 对脚本插件的缓存支持极弱,每次构建都重新解析;二进制插件可被构建缓存命中,尤其在 CI 上差异明显
为什么 buildSrc 不算“真正”的自定义插件?
buildSrc 是个特例:它会被 Gradle 自动编译成 jar 并加入构建脚本的 classpath,所以你能在里面写 class MyPlugin implements Plugin<project></project>,然后在子项目里 apply plugin: MyPlugin。但它仍受限于单个项目——不能发版、不能被其他 Git 仓库引用、无法设置 pluginBundle 元信息。更关键的是,buildSrc 的任何改动都会触发整个构建脚本重加载,容易掩盖真实的问题来源。真要复用,不如直接拆成独立的插件项目,用 publishToMavenLocal 测试后再推到私有 Nexus。
容易踩的坑:插件 ID 和插件类名不匹配
Gradle 要求二进制插件的 ID 必须和 META-INF/gradle-plugins/*.properties 文件里的键完全一致,且该文件值必须指向具体实现类的全限定名。常见错误包括:
- 写了
com.example.myplugin=MyPlugin,但实际类是com.example.MyBuildPlugin—— 运行时抛Could not find implementation class - 用了 Kotlin 插件但没加
@Suppress("unused"),Kotlin 编译器把顶层函数或伴生对象优化掉了,导致 properties 文件指向的类不存在 - 在插件项目里误用了
implementation依赖了gradleApi(),而不是compileOnly—— 导致发布后的 jar 混入 Gradle 内部类,运行时报NoClassDefFoundError
真正难的从来不是“怎么写出来”,而是让插件在不同 Gradle 版本、不同 JDK、不同项目结构下稳定工作——类加载顺序、Project 实例生命周期、extension 注册时机,这些细节一旦错位,错误往往延迟到执行阶段才暴露,排查成本远高于脚本里直接写死逻辑。











