30、实战项目:从零构建一个完整插件、插件功能设计、测试与发布、文档编写、社区维护
终于到了这门课的压轴章节。说实话,前面讲了那么多理论、API、生命周期,都是为了这一刻——从零开始,亲手打造一个能用的、能发布的 Gradle 插件。
我个人习惯把插件开发分成五个阶段:功能设计 → 编码实现 → 测试验证 → 发布上架 → 文档与社区。今天我们就走一遍完整的流程。你跟着我做一遍,以后遇到任何插件需求,心里都有底。
一、插件功能设计:先想清楚再动手
很多新手一上来就写代码,结果写到一半发现设计有漏洞,回头改结构,浪费时间。我建议你先花 30 分钟把功能想透。
我们这次要做的插件叫 BuildReporterPlugin,核心功能是:在每次构建完成后,自动生成一份构建报告,包含构建耗时、任务执行顺序、警告和错误数量。
1.1 功能清单
- 监听构建结束事件(
BuildListener) - 收集构建耗时、任务执行时间
- 统计警告和错误数量
- 输出 HTML 格式的报告到
build/reports/目录 - 支持通过扩展配置开关(是否启用、输出路径)
1.2 扩展配置设计
我习惯先定义好用户怎么用这个插件。比如这样:
buildReporter {
enabled = true
outputDir = file("build/reports")
includeTaskTimings = true
}
对应的 Kotlin 扩展类:
open class BuildReporterExtension {
var enabled: Boolean = true
var outputDir: File? = null
var includeTaskTimings: Boolean = true
}
二、从零构建插件:核心代码实现
好,设计完了,开始写代码。我们创建一个独立的 Gradle 模块,用 java-gradle-plugin 和 kotlin 插件。
2.1 项目结构
build-reporter-plugin/
├── build.gradle.kts
├── src/
│ └── main/
│ └── kotlin/
│ └── com/example/buildreporter/
│ ├── BuildReporterPlugin.kt
│ ├── BuildReporterExtension.kt
│ └── ReportGenerator.kt
└── src/
└── functionalTest/
└── kotlin/
└── BuildReporterPluginTest.kt
2.2 插件入口
class BuildReporterPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(
"buildReporter", BuildReporterExtension::class.java
)
project.gradle.addBuildListener(object : BuildListener {
override fun buildFinished(result: BuildResult) {
if (!extension.enabled) return
val report = ReportGenerator.generate(project, result)
val outputFile = extension.outputDir
?: project.layout.buildDirectory.dir("reports").get().asFile
report.writeTo(outputFile)
}
override fun projectsLoaded(gradle: Gradle) {}
override fun projectsEvaluated(gradle: Gradle) {}
override fun buildStarted(gradle: Gradle) {}
})
}
}
buildFinished 里直接操作文件,结果发现项目还没完全关闭,导致文件锁冲突。后来加了个 project.afterEvaluate 延迟执行,问题解决。
2.3 报告生成器
object ReportGenerator {
fun generate(project: Project, result: BuildResult): Report {
val duration = result.action?.let {
project.gradle.buildFinishedTime - project.gradle.startTime
} ?: 0L
val warnings = result.failures?.count { it.isWarning } ?: 0
val errors = result.failures?.count { !it.isWarning } ?: 0
return Report(
projectName = project.name,
buildDurationMs = duration,
warningCount = warnings,
errorCount = errors,
taskTimings = collectTaskTimings(project)
)
}
private fun collectTaskTimings(project: Project): List<TaskTiming> {
return project.gradle.taskGraph.allTasks.map { task ->
TaskTiming(
taskName = task.name,
durationMs = task.state.executed ?: 0L
)
}
}
}
三、测试与验证:别让插件带着 bug 上线
写代码只占 40% 的时间,测试占 40%,剩下 20% 是文档和发布。我见过太多人跳过测试,结果发布后用户一用就崩。
3.1 单元测试
用 JUnit 5 + Gradle TestKit 做功能测试:
class BuildReporterPluginFunctionalTest {
@TempDir
lateinit var testProjectDir: File
@Test
fun `插件应生成报告文件`() {
val buildFile = File(testProjectDir, "build.gradle.kts")
buildFile.writeText("""
plugins {
id("com.example.buildreporter")
}
buildReporter {
enabled = true
}
""".trimIndent())
val result = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments("build")
.withPluginClasspath()
.build()
val reportFile = File(testProjectDir, "build/reports/build-report.html")
assertTrue(reportFile.exists())
}
}
@TempDir 创建临时项目目录,避免测试之间互相污染。我早期没注意这个,两个测试共享同一个目录,结果数据全乱了。
3.2 手动验证清单
| 检查项 | 预期结果 |
|---|---|
| 插件应用后构建正常 | 无报错,构建成功 |
| 报告文件生成 | build/reports/build-report.html 存在 |
| 报告内容正确 | 包含项目名、耗时、任务列表 |
| 禁用插件后不生成 | enabled = false 时无报告 |
| 自定义输出路径 | 文件生成到指定目录 |
四、发布到 Gradle Plugin Portal
测试通过后,就可以发布了。我建议用 maven-publish 插件配合 com.gradle.plugin-publish 插件。
4.1 配置发布脚本
plugins {
`java-gradle-plugin`
`maven-publish`
id("com.gradle.plugin-publish") version "1.2.1"
}
pluginBundle {
website = "https://github.com/your/repo"
vcsUrl = "https://github.com/your/repo.git"
tags = listOf("build", "report", "android")
}
gradlePlugin {
plugins {
register("buildReporter") {
id = "com.example.buildreporter"
displayName = "Build Reporter Plugin"
description = "自动生成构建报告"
implementationClass = "com.example.buildreporter.BuildReporterPlugin"
}
}
}
4.2 发布命令
./gradlew publishPlugins
publishToMavenLocal 测试一遍。确认无误后再上传到 Portal。我曾经因为 groupId 写错,导致发布后无法使用,还得联系管理员删除,折腾了两天。
五、文档编写:让用户一看就懂
文档是插件的门面。我见过功能很强大的插件,因为文档写得烂,没人用。反过来,功能一般的插件,文档写得好,用户反而觉得专业。
5.1 文档结构
- README.md:一句话介绍、快速开始、配置示例
- CHANGELOG.md:版本更新记录
- CONTRIBUTING.md:贡献指南
- docs/ 目录:详细文档,包含 API 参考、常见问题
5.2 快速开始示例
// build.gradle.kts
plugins {
id("com.example.buildreporter") version "1.0.0"
}
buildReporter {
enabled = true
outputDir = file("build/my-reports")
}
然后执行 ./gradlew build,报告就会自动生成。就这么简单。
六、社区维护:让插件活起来
插件发布出去只是开始。真正的挑战是维护。我维护过三个开源插件,踩了不少坑,分享几点经验。
6.1 Issue 管理
- 给 Issue 打标签:
bug、enhancement、question - 48 小时内回复,哪怕只是说「收到,我看看」
- 用模板引导用户提供必要信息(Gradle 版本、插件版本、错误日志)
6.2 版本策略
| 版本号 | 含义 | 示例 |
|---|---|---|
| 1.0.0 | 正式版 | 第一个稳定版本 |
| 1.1.0 | 新增功能 | 添加任务耗时统计 |
| 1.1.1 | Bug 修复 | 修复文件锁问题 |
6.3 持续集成
用 GitHub Actions 自动跑测试、发布。每次提交都触发 CI,确保质量。
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
java-version: '17'
- run: ./gradlew check
七、知识体系总览
下面这张图总结了从零构建插件的完整流程。你可以把它当作一个检查清单,每完成一步就打个勾。
嗯,到这里,一个完整的插件从设计到发布再到维护的全流程就走完了。你可能会觉得步骤有点多,但相信我,每多做一次,速度就会快一倍。我第一次做这个流程花了整整一周,现在半天就能搞定。
最后说一句:插件开发最难的从来不是写代码,而是想清楚「用户到底需要什么」。把这个问题想透了,剩下的都是体力活。
公众号:蓝海资料掘金营,微信deep3321