Kotlin 编译器插件:KSP 与编译器前端扩展机制深度
从 KSP 架构、Symbol Processor 生命周期、KSVisitor 访问者模式出发,解析 Kotlin 编译插件的扩展机制,以及实战中的最佳实践。
1. KSP 架构与定位
1.1 KSP vs KAPT
Kotlin Symbol Processing(KSP)是 Kotlin 官方推出的注解处理器替代方案。相比 KAPT(Kotlin Annotation Processing Tool),KSP 直接操作 Kotlin 编译器前端的符号表,不需要启动 JVM 反射或 Java 注解处理:
| 维度 | KAPT | KSP |
|---|---|---|
| 架构 | Java APT + JVM 反射 | 直接访问 Kotlin 符号 |
| 依赖 | kotlin-reflect(JVM) | 无反射,直接符号查询 |
| 速度 | 慢(启动开销大) | 快(2-3x vs KAPT) |
| KMP 支持 | ❌ 仅 JVM | ✅ 全部平台 |
| 增量编译 | 有限支持 | 完整增量支持 |
1.2 KSP 编译流程
2. Symbol Processor 生命周期
2.1 SymbolProcessor 接口
KSP 的核心是 SymbolProcessor 接口,所有处理器都实现这个接口:
interface SymbolProcessor {
// 处理函数:在所有符号解析完成后调用
// resolver.getSymbolsWithAnnotation() 返回的符号在这里处理
fun process(resolver: Resolver): List<KSNode>
// finish:在编译结束时调用,用于生成文件
fun finish()
// onError:处理过程中遇到错误时调用
fun onError(error: KSError)
}
// 创建处理器
class MyProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSNode> {
// 获取所有标记了 @MyAnnotation 的符号
val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnnotation")
symbols.filter { it is KSClassDeclaration }
.forEach { processClass(it) }
return emptyList()
}
}
2.2 Resolver 与符号查询
Resolver 是 KSP 中最重要的对象,它提供了对编译上下文中所有符号的查询能力:
// Resolver 提供的查询方法
// 1. 按注解查询
Resolvers.getSymbolsWithAnnotation("com.example.MyAnnotation")
// 2. 按包名查询所有类
Resolver.getClassDeclarationsInPackage("com.example")
// 3. 查找类型引用
Resolver.getTypeArguments(typeRef: KSTypeReference)
// 4. 扩展函数:获取直接子类型
Resolver.getDirectSubclasses(ksClass: KSClassDeclaration)
// 5. 创建类型 alias
Resolver.createKSType(javaType: Type)
2.3 处理器执行顺序
KSP 通过 processorPath 确定处理器执行顺序。使用 @AutoCanonical 或 @Order 控制:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xplugin=" +
"$kspPluginJar${File.pathSeparator}$otherPluginJar")
freeCompilerArgs.add("-Xallow.any.target.in.source.root")
}
}
// 代码中指定顺序
@AutoCanonical
class FirstProcessor : SymbolProcessor { ... }
@AutoCanonical
class SecondProcessor : SymbolProcessor { ... }
// FirstProcessor 会在 SecondProcessor 之前执行
3. KSVisitor 访问者模式
3.1 KSVisitor 接口层次
KSP 使用 Visitor 模式遍历 AST。所有的 Visitor 接口都继承自 KSVisitor<T, R>:
// KSVisitor<T, R> 定义
// T: 被访问的节点类型(如 KSClassDeclaration)
// R: visit 方法的返回值类型
interface KSVisitor<T, R> {
fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: T): R
fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: T): R
fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: T): R
fun visitAnnotation(annotation: KSAnnotation, data: T): R
// ... 其他 visit 方法
}
// 具体 Visitor 实现示例
class MyVisitor : KSVisitorVoid {
override fun visitClassDeclaration(decl: KSClassDeclaration, data: Unit) {
println("Found class: ${decl.simpleName}")
decl.getDeclaredProperties().forEach { it.accept(this, Unit) }
decl.getDeclaredFunctions().forEach { it.accept(this, Unit) }
}
}
3.2 常用 KSNode 类型
KSP 中的符号节点类型:
| 类型 | 说明 | 可获取信息 |
|---|---|---|
KSClassDeclaration |
类/接口/对象声明 | 名称、父类、成员 |
KSFunctionDeclaration |
函数声明 | 参数、返回值、注解 |
KSPropertyDeclaration |
属性声明 | 类型、初始值、getter/setter |
KSAnnotation |
注解 | 注解类型、参数值 |
KSTypeReference |
类型引用 | 解析后的类型 |
4. 编译插件扩展机制
4.1 Kotlin Compiler Plugin 体系
Kotlin 编译器支持三种扩展点:
// 1. IR 变换(IR Plugin)
// 插入到 Kotlin → IR → Bytecode 流水线
// Compose Compiler 就是 IR Plugin
// 2. 编译检查(Check Plugin)
// 在编译器前端执行类型检查
// 可以添加自定义警告/错误
// 3. KSP(Symbol Processor)
// 在编译前期处理注解和符号
// 生成代码或修改 AST
// 编译插件注册(plugin.yml)
// - name: org.jetbrains.kotlin.compose.compilerplugins
// embeddedIntoCompiler: true
// changesCompilation: true
4.2 编写自定义 KSP 处理器
实战:实现一个生成 equals/hashCode/toString 的处理器:
// 1. 定义注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoData
// 2. 实现 SymbolProcessor
class AutoDataProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSNode> {
val symbols = resolver.getSymbolsWithAnnotation(AutoData::class.qualifiedName)
symbols.filter { it is KSClassDeclaration }
.forEach { generateDataClass(it) }
return symbols.toList()
}
private fun generateDataClass(decl: KSClassDeclaration) {
// 生成代码...
KSFile.createKSFile(...).appendText(generatedCode)
}
}
// 3. 注册服务
// META-INF/services/com.google.devtools.ksp.SymbolProcessor
// com.example.AutoDataProcessor
5. 实战与最佳实践
5.1 KSP 性能优化
KSP 相比 KAPT 更快,但仍有优化空间:
✅ KSP 性能建议
- 使用
resolver.getSymbolsWithAnnotation()而非全量扫描 - 增量处理:利用
KSNode.containingFile确定变更范围 - 避免深度递归遍历,优先使用 Visitor
- 缓存已解析的类型引用
❌ 常见性能问题
- 在 process() 中调用
resolve()触发完整类型解析 - 全量扫描所有类声明(大量 I/O)
- 生成过多文件导致增量编译失效
- 不使用
@GeneratedAnnotation丢失增量标记
5.2 实际案例:Room KSP 迁移
Google 的 Room 数据库从 KAPT 迁移到 KSP 后,编译速度提升约 2-3x,增量编译支持也大幅改善。以下是关键迁移步骤:
- 将
kapt插件替换为ksp - 处理器从 Java 反射改为 KSP Symbol API
- 使用
KSClassDeclaration.getValueParameters()获取构造参数 - 使用
KSTypeReference.resolve()获取完整类型
# KSP 迁移评估
# □ 确认所有依赖库都有 KSP 版本
# □ 处理器不依赖 JVM 反射
# □ 检查 KSVisitor 是否覆盖所有节点类型
# □ 添加 @GeneratedAnnotation 支持增量
# □ 测试增量编译场景
# □ 验证所有平台的生成代码一致性