26、自定义Gradle插件:集成KSP/KAPT、配置注解处理器依赖

说实话,注解处理器在Android项目里已经是个标配了。不管是ButterKnife、Glide还是Room,背后都离不开APT或者KSP。但问题来了——每次新建项目都要手动配一堆依赖,版本号还容易写错,团队里有人漏配了某个参数,编译直接挂掉。

我个人习惯是把这些重复劳动封装成一个Gradle插件。你想想看,一行apply plugin,所有注解处理器自动就位,多省心。今天我们就来聊聊怎么自定义一个Gradle插件,把KSP和KAPT的集成工作彻底自动化。

为什么需要自定义插件?

我在项目中遇到过好几次这样的场景:项目里有十几个模块,每个模块都要配Room、Glide、Moshi的注解处理器。有人漏了kapt { correctErrorTypes = true },有人版本号写成了旧版,编译报错排查半天。

说白了,手动配置就是容易出错。自定义插件能帮你做到三件事:

  • 统一版本管理——所有注解处理器版本号写在插件里,一处修改全局生效
  • 自动配置参数——KAPT的correctErrorTypes、KSP的blockOtherPlugins,插件帮你写好
  • 按需启用——根据模块类型(application/library)自动判断是否启用KAPT

插件项目结构

我们先搭一个标准的Gradle插件项目。嗯,这里要注意,插件本身要用Java或者Kotlin写,我推荐用Kotlin,毕竟跟Android项目语言一致。

// 项目根目录
annotation-processor-plugin/
├── build.gradle.kts
├── src/
│   └── main/
│       └── kotlin/
│           └── com/example/plugin/
│               ├── AnnotationProcessorPlugin.kt
│               └── AnnotationProcessorExtension.kt
└── src/
    └── main/
        └── resources/
            └── META-INF/
                └── gradle-plugins/
                    └── com.example.annotation-processor.properties

插件的build.gradle.kts要引入java-gradle-pluginkotlin插件。我个人习惯用implementation(gradleApi())来依赖Gradle API,这样最干净。

// build.gradle.kts
plugins {
    `java-gradle-plugin`
    kotlin("jvm") version "1.9.0"
}

dependencies {
    implementation(gradleApi())
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0")
}

gradlePlugin {
    plugins {
        register("annotationProcessor") {
            id = "com.example.annotation-processor"
            implementationClass = "com.example.plugin.AnnotationProcessorPlugin"
        }
    }
}

核心逻辑:集成KSP和KAPT

插件的主类要干两件事:一是把KSP/KAPT插件apply到目标项目,二是配置注解处理器的依赖。我写了一个简化版本,你看一下核心思路:

// AnnotationProcessorPlugin.kt
class AnnotationProcessorPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // 1. 创建扩展,让用户能配置版本号
        val extension = project.extensions.create(
            "annotationProcessor",
            AnnotationProcessorExtension::class.java
        )

        // 2. 判断是否Android项目
        if (!project.plugins.hasPlugin("com.android.application") &&
            !project.plugins.hasPlugin("com.android.library")) {
            return
        }

        // 3. 根据配置选择KAPT或KSP
        project.afterEvaluate {
            val config = extension.get()
            if (config.useKsp) {
                applyKsp(project, config)
            } else {
                applyKapt(project, config)
            }
        }
    }

    private fun applyKsp(project: Project, config: AnnotationProcessorConfig) {
        // 应用KSP插件
        project.plugins.apply("com.google.devtools.ksp")

        // 配置依赖
        project.dependencies.add("ksp", "androidx.room:room-compiler:${config.roomVersion}")
        project.dependencies.add("ksp", "com.github.bumptech.glide:compiler:${config.glideVersion}")
    }

    private fun applyKapt(project: Project, config: AnnotationProcessorConfig) {
        // 应用KAPT插件
        project.plugins.apply("org.jetbrains.kotlin.kapt")

        // 配置KAPT参数
        project.extensions.configure<KaptExtension>("kapt") {
            correctErrorTypes = config.correctErrorTypes
        }

        // 添加依赖
        project.dependencies.add("kapt", "androidx.room:room-compiler:${config.roomVersion}")
        project.dependencies.add("kapt", "com.github.bumptech.glide:compiler:${config.glideVersion}")
    }
}

你可能会问,为什么用afterEvaluate?因为插件apply的时候,Android的buildTypesproductFlavors还没配置好,直接加依赖可能会漏掉某些变体。等评估完再配,稳一点。

扩展配置:让用户自定义

插件不能写死版本号,得留个口子让用户改。我设计了一个AnnotationProcessorExtension,用open属性让用户覆盖:

// AnnotationProcessorExtension.kt
open class AnnotationProcessorExtension {

    var useKsp: Boolean = false
    var correctErrorTypes: Boolean = true
    var roomVersion: String = "2.6.0"
    var glideVersion: String = "4.15.1"
    var moshiVersion: String = "1.15.0"

    fun get(): AnnotationProcessorConfig {
        return AnnotationProcessorConfig(
            useKsp = useKsp,
            correctErrorTypes = correctErrorTypes,
            roomVersion = roomVersion,
            glideVersion = glideVersion,
            moshiVersion = moshiVersion
        )
    }
}

data class AnnotationProcessorConfig(
    val useKsp: Boolean,
    val correctErrorTypes: Boolean,
    val roomVersion: String,
    val glideVersion: String,
    val moshiVersion: String
)

用户在项目的build.gradle.kts里这样用:

plugins {
    id("com.example.annotation-processor")
}

annotationProcessor {
    useKsp = true
    roomVersion = "2.6.1"
    glideVersion = "4.16.0"
}

你看,一行插件声明,三行配置,所有注解处理器自动就位。团队里再也不会有人漏配了。

避坑指南:我曾经踩过的坑

我曾经在配置KSP的时候,忘了设置blockOtherPlugins参数,结果KSP和KAPT同时生效,编译直接报重复类定义。后来我加了一个检查:如果用户同时启用了KSP和KAPT,插件直接抛异常。

注意:KSP和KAPT不能同时用于同一个注解处理器。Room官方已经明确推荐KSP,如果你还在用KAPT,建议尽快迁移。性能差距不是一点半点——KSP编译速度能快2-3倍。

还有一个坑:KSP的依赖配置名是ksp,不是kapt。如果你写错了,依赖不会报错,但注解处理器根本不生效。我建议在插件里加一个日志输出,打印出最终添加的依赖列表,方便排查。

知识体系总览

下面这张图把整个插件的设计思路串起来了,你看一眼就能明白各个模块的关系:

自定义Gradle插件:集成KSP/KAPT AnnotationProcessorPlugin AnnotationProcessorExtension KSP 分支 apply plugin: com.google.devtools.ksp dependencies.add("ksp", ...) KAPT 分支 apply plugin: org.jetbrains.kotlin.kapt dependencies.add("kapt", ...) 插件根据 useKsp 配置自动选择分支,统一管理版本号和参数

发布与使用

插件写好了,怎么发布?我一般用maven-publish插件发布到公司内部的Nexus仓库。你也可以发布到Gradle Plugin Portal,让全世界的人用。

// 发布配置
plugins {
    `maven-publish`
}

publishing {
    publications {
        create<MavenPublication>("plugin") {
            from(components["java"])
            groupId = "com.example"
            artifactId = "annotation-processor-plugin"
            version = "1.0.0"
        }
    }
    repositories {
        maven {
            url = uri("https://your-nexus.com/repository/maven-releases/")
        }
    }
}

项目里引用的时候,先在settings.gradle.kts里声明插件源:

pluginManagement {
    repositories {
        maven { url = uri("https://your-nexus.com/repository/maven-releases/") }
        gradlePluginPortal()
    }
}

然后在模块的build.gradle.kts里直接apply:

plugins {
    id("com.example.annotation-processor") version "1.0.0"
}
小技巧:如果你用的是Kotlin DSL,插件ID和版本号可以提取到gradle.properties里,方便统一升级。比如写一个annotationProcessorVersion=1.0.0,然后在build.gradle.kts里用version = annotationProcessorVersion引用。

总结

自定义Gradle插件来集成KSP/KAPT,说白了就是把重复劳动自动化。你只需要写一次插件逻辑,团队里所有人都能受益。而且插件可以随着项目演进不断迭代——比如新增一个注解处理器、调整默认版本号,改一行代码,所有模块自动同步。

我个人觉得,这种"基础设施"类的投入,性价比非常高。花半天时间写个插件,能省下未来无数个"编译报错排查"的下午。嗯,如果你还没试过,建议从一个小模块开始,把Room或者Glide的注解处理器抽出来,感受一下自动化的快感。


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