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-pluginkotlin 插件。

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 打标签:bugenhancementquestion
  • 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

七、知识体系总览

下面这张图总结了从零构建插件的完整流程。你可以把它当作一个检查清单,每完成一步就打个勾。

插件开发全流程 功能设计 扩展配置、监听事件 编码实现 Plugin、Extension、Report 测试验证 单元测试、功能测试 发布上架 Plugin Portal、Maven 文档编写 README、API、FAQ 社区维护 Issue、PR、版本迭代 持续迭代循环 核心原则 先设计后编码 · 测试覆盖核心路径 · 文档与代码同步更新 · 社区反馈驱动迭代

嗯,到这里,一个完整的插件从设计到发布再到维护的全流程就走完了。你可能会觉得步骤有点多,但相信我,每多做一次,速度就会快一倍。我第一次做这个流程花了整整一周,现在半天就能搞定。

最后说一句:插件开发最难的从来不是写代码,而是想清楚「用户到底需要什么」。把这个问题想透了,剩下的都是体力活。

总结:功能设计要简洁,编码要规范,测试要全面,文档要清晰,社区要活跃。这五件事做好了,你的插件就能真正帮到别人。

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