上一篇聊了代码质量保障,这篇钻进 Kotlin 编译器插件。Compose 的 @Composable、KSP 的 @Inject、Room 的 @Dao 都是编译器插件的产物。理解它,才能从"调 API"升级到"造 API"。
什么是 Kotlin 编译器插件
Kotlin 编译器分 Frontend(解析、类型检查)和 Backend(生成字节码)两段,插件可以介入多个阶段:
@Composable 的实现就是 IR 阶段:编译器把函数体包一层 Composer.startReplaceableGroup(...),跟踪调用顺序。
用 KSP 还是编译器插件
新同学分不清这两个工具:
KSP(Kotlin Symbol Processing):基于 Kotlin 语法的轻量级注解处理器。简单、快、能访问 AST,但改不了函数体。常用于生成代码(Room、Glide、Hilt 部分功能)。
Kotlin Compiler Plugin:完整的编译器扩展。能改 IR,能加新语法(如 @Composable)。复杂、慢、版本敏感,但无所不能。
选 KSP 除非你真的需要改函数体。
写一个最简 KSP 处理器
环境准备:
// build.gradle.kts (project)
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
// build.gradle.kts (module)
plugins {
id("com.google.devtools.ksp")
kotlin("jvm") // 或 android
}
dependencies {
implementation("com.google.devtools.ksp:com.google.devtools.ksp.symbol-processing-api:2.0.21-1.0.27")
}
Processor 代码:
// src/main/kotlin/com/example/builder/BuilderProcessor.kt
package com.example.builder
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate.*
class BuilderProcessor(
environment: SymbolProcessorEnvironment
) : SymbolProcessor {
private val logger = environment.logger
override fun process(resolver: Resolver): List<KSAnnotated> {
// 找出所有 @Builder 注解的类
val symbols = resolver.getSymbolsWithAnnotation("com.example.Builder")
symbols.forEach { ksAnnotated ->
when (ksAnnotated) {
is KSClassDeclaration -> generateBuilder(ksAnnotated)
}
}
// 返回未处理的符号,让其他 processor 处理
return emptyList()
}
private fun generateBuilder(cls: KSClassDeclaration) {
val packageName = cls.packageName.asString()
val className = cls.simpleName.asString()
val builderName = "${className}Builder"
val fields = cls.primaryConstructor?.parameters ?: return
val fieldDeclarations = fields.joinToString("\n") { param ->
" private var ${param.name?.asString()}: ${param.type.resolve().declaration.qualifiedName?.asString()}? = null"
}
val setters = fields.joinToString("\n") { param ->
val name = param.name?.asString()
val type = param.type.resolve().declaration.qualifiedName?.asString()
" fun $name(value: $type) = apply { this.$name = value }"
}
val params = fields.joinToString(", ") { it.name?.asString() }
val assigns = fields.joinToString("\n ") {
" ${it.name?.asString()} = ${it.name?.asString()}!!"
}
val code = """
package $packageName
class $builderName {
$fieldDeclarations
$setters
fun build(): $className = $className(
$assigns
)
}
""".trimIndent()
// 写到 generated sources 目录
// (实际用 environment.codeGenerator.createNewFile(...))
logger.info("Generated $builderName")
}
}
// ProcessorProvider 注册
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
BuilderProcessor(environment)
}
注册:
// src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
com.example.BuilderProcessorProvider
使用:
@Builder
data class User(
val name: String,
val age: Int
)
// 生成 UserBuilder
val user = UserBuilder()
.name("Alice")
.age(30)
.build()
输出文件而不是打印日志
实际项目要把生成的代码写到文件:
private fun generateBuilder(cls: KSClassDeclaration) {
val codeGenerator = environment.codeGenerator // 需要传进来
val file = codeGenerator.createNewFile(
dependencies = Dependencies(false, cls.containingFile!!),
packageName = cls.packageName.asString(),
fileName = "${cls.simpleName.asString()}Builder"
)
file.bufferedWriter().use { writer ->
writer.write("""
package ${cls.packageName.asString()}
class ${cls.simpleName.asString()}Builder {
${fields.joinToString("\n") { ... }}
}
""".trimIndent())
}
}
调试 KSP
KSP 默认不输出到控制台,加参数看日志:
./gradlew :app:kspDebugKotlin --info
或者通过 Logger 输出:
environment.logger.warn("Generated: $className")
IntelliJ 中断点调试 KSP:
// processor 中加
if (System.getProperty("ksp.debug") == "true") {
Thread.sleep(10000) // 等待 10 秒,趁机 attach
}
用 jstack 找到 KSP 进程,用 IDE 的 Attach to Process 功能连上去设断点。
Kotlin Compiler Plugin 的真相
KSP 写处理器很简单,但 Compose 的 @Composable 必须在编译器 IR 阶段改函数体。这要用 Kotlin Compiler Plugin。
插件结构:
my-plugin/
├── gradle.properties
├── build.gradle.kts
└── src/main/kotlin/
└── com/example/myplugin/
├── MyPlugin.kt // CompilerPluginRegistrar
├── MyIrGenerationExtension.kt // 改 IR
└── components/
└── MyIrTransformer.kt // 实际转换
CompilerPluginRegistrar:
package com.example.myplugin
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.*
@CompilerPluginRegistrar(
version = "1.0.0",
componentStorageComponents = []
)
class MyPluginRegistrar : CompilerPluginRegistrar() {
override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
IrGenerationExtension.registerExtension(MyIrExtension())
}
}
IrGenerationExtension 是真正改函数体的地方,需要遍历 IrModuleFragment:
class MyIrExtension : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
moduleFragment.accept(IrTransformer(pluginContext), null)
}
}
写这一步要懂 IR API(IrFunction、IrCall、IrExpressionBody 等),通常参考已有的开源项目(kotlin-publish-subscribe-plugin 等)。
实战建议
新需求先看 KSP:90% 的注解处理器用 KSP 足够,调试简单
慎用 compiler plugin:跟 Kotlin 版本强绑定,每次升级 Kotlin 都要重新适配
测试用
kspTest:KSP 提供KSPTestRunner用 JVM 跑测试文档和示例少:Compiler Plugin 文档稀缺,主要靠看 Compose 的源码学习
小结
KSP 是日常 Android 开发的实用工具,能写自定义注解处理器、自动生成样板代码(Builder、Factory、Mapper)。Compiler Plugin 是登堂入室级别的工具,需要懂 IR、依赖底层 API,门槛很高但能做 Compose 级别的魔法。建议先用 KSP 解决实际问题,需要时再深入 compiler plugin。
下一篇聊聊 ASM 字节码插桩,从 KSP 的"写文件"升级到"改字节码"。