在之前的文章一文了解 apt、 kapt 、 ksp 和 kcp 中,我们介绍了 apt、kapt、KSP 以及 kcp 的区别,这篇文章将介绍 ksp的使用。
创建模块
首先我们需要先创建一个 Java or Kotlin Library
模块,如下图所示:
创建完模块后,在 ksp 模块下的 build.gradle.kts
文件中增加如下的配置:
plugins {
kotlin("jvm")
}
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.0-1.0.11")
implementation("com.squareup:kotlinpoet-ksp:1.18.1")
}
其中 com.google.devtools.ksp:symbol-processing-api
是 ksp 提供的功能接口,而 1.9.0-1.0.11
中 1.9.0
是指 KSP 插件所依赖的 Kotlin 语言或者相关基础库的版本,而1.0.11
表示是 KSP 插件自身的版本。所有支持的 ksp 版本可以见 Ksp Releases
创建 SymbolProcessorProvider 和 SymbolProcessor
在 Ksp 中,具体的代码生成逻辑是通过 SymbolProcessorProvider
和 SymbolProcessor
来实现的。这里以实现一个 打印使用了 @TestFind
注解的函数的类名和函数名的类 为例。
首先我们先创建一个@TestFind
注解,ksp 通过这个注解来获取使用该注解的类名和函数名:
annotation class TestFind()
然后实现 SymbolProcessorProvider
接口,它的作用是 SymbolProcessorEnvironment
的提供者。代码示例如下:
/**
* SymbolProcessorProvider 是环境的提供者,主要为我们符号处理器SymbolProcessor提供了必要的环境(SymbolProcessorEnvironment),
* SymbolProcessorEnvironment 中最重要的是 codeGenerator 和 logger,它们的作用如下:
* codeGenerator:用于用于生成与管理文件
* logger:用于将日志输出到构建结果中
*/
class MySymbolProcessorProvider: SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return MyProcessor(environment.codeGenerator, environment.logger)
}
}
最后实现 SymbolProcessor
接口,其内部就是具体的代码实现了。代码示例如下:
class MyProcessor(
// 用于生成代码文件的对象,通过它可以创建新的代码文件并将生成的代码写入其中
private val codeGenerator: CodeGenerator,
// 用于在处理过程中输出日志信息,方便调试和记录处理状态
private val logger: KSPLogger
) : SymbolProcessor {
companion object {
@Volatile
var isInit = false
}
/**
* 符号处理的核心方法,负责查找使用了 TestFind 注解的函数,并生成相应的结果类。
* @param resolver 用于解析代码符号的对象,可通过它获取代码中的各种符号信息
* @return 返回的是一个list集合,代表着那些不需要被修改的符号,因为我们注解修饰的符号不一定都有效
*/
override fun process(resolver: Resolver): List<KSAnnotated> {
// 获取所有使用了 TestFind 注解的符号
val mySymbol = resolver.getSymbolsWithAnnotation(TestFind::class.qualifiedName!!)
// 过滤出无效的符号并存储在 ret 列表中,后续可能需要对这些无效符号进行特殊处理
val ret = mySymbol.filter { !it.validate() }.toList()
// 从使用 TestFind 注解的符号中筛选出函数声明的符号,并转换为 KSFunctionDeclaration 类型的列表
val list = mySymbol.filter {
it is KSFunctionDeclaration
}.map {
it as KSFunctionDeclaration
}.toList()
// 用于存储使用了 TestFind 注解的函数所在的类名和函数名的键值对
val results = mutableListOf<Pair<String, String>>()
// 遍历筛选出的函数声明列表
list.forEach { function ->
// 获取函数所在类的全限定名,如果获取不到则使用 "Unknown" 替代
val className = function.parentDeclaration?.qualifiedName?.asString() ?: "Unknown"
// 获取函数的简单名称
val methodName = function.simpleName.asString()
// 将类名和函数名组成的键值对添加到 results 列表中
results.add(Pair(className, methodName))
}
// 在日志中输出包含使用了 TestFind 注解的函数的类名和函数名的列表
logger.warn("list is ${results.toList()}")
// 调用生成 TestFindResult 类的方法
generateTestFindResultClass(results)
// 返回无效的符号列表
return ret
}
/**
* 生成 TestFindResult 类,该类包含一个方法用于打印使用了 TestFind 注解的函数的类名和函数名。
*/
private fun generateTestFindResultClass(results: List<Pair<String, String>>) {
// 如果已经初始化生成过 TestFindResult 类,则直接返回,避免重复生成
if (isInit) {
return
}
// 标记为已初始化
isInit = true
// 使用 KotlinPoet 构建一个代码文件,指定包名和文件名
val fileSpec = FileSpec.builder("com.example.result", "TestFindResult")
.addType(
// 构建 TestFindResult 类
TypeSpec.classBuilder("TestFindResult")
.addFunction(
// 构建 printAnnotatedMethods 方法
FunSpec.builder("printAnnotatedMethods")
.addCode(buildString {
// 遍历 results 列表,将每个键值对的类名和函数名添加到代码字符串中
results.forEach { (className, methodName) ->
appendLine("println(\"Class: $className, Method: $methodName\")")
}
})
.build()
)
.build()
)
.build()
// 将构建好的代码文件写入到代码生成器中,第二个参数 false 表示不依赖其他文件
fileSpec.writeTo(codeGenerator, false)
}
}
对于代码结构解析,可以看到核心是 SymbolProcessor
的 process
方法传递过来的 Resolver
对象。我们可以通过这个对象来处理 kotlin 源代码。 具体解析 kt 源代码文件的结构如下所示:
KSFile
packageName: KSName
fileName: String
annotations: List<KSAnnotation> // 源代码文件注解
declarations: List<KSDeclaration>
KSClassDeclaration // 类, 接口, 对象
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
classKind: ClassKind
primaryConstructor: KSFunctionDeclaration
superTypes: List<KSTypeReference>
// 包含内部类, 成员函数, 属性, 等等.
declarations: List<KSDeclaration>
KSFunctionDeclaration // 顶层函数
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
functionKind: FunctionKind
extensionReceiver: KSTypeReference?
returnType: KSTypeReference
parameters: List<KSValueParameter>
// 包含局部类, 局部函数, 局部变量, 等等.
declarations: List<KSDeclaration>
KSPropertyDeclaration // 全局变量
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
extensionReceiver: KSTypeReference?
type: KSTypeReference
getter: KSPropertyGetter
returnType: KSTypeReference
setter: KSPropertySetter
parameter: KSValueParameter
对于代码生成,这里使用了 KotlinPoet 来生成对应的 kotlin 代码。关于 KotlinPoet 的使用可以看KotlinPoet 文档。
定义服务
由于 ksp 是基于 java spi 机制实现,因此需要在 resource/META-INF/services
目录下定义一个com.google.devtools.ksp.processing.SymbolProcessorProvider
文件,并写入创建的创建的SymbolProcessorProvider服务的全类名。关于java spi 机制可以看 Java SPI 机制详解 | JavaGuide。
需要注意:在Android studio 中创建 META-INF/services 时,建议先创建 META-INF 目录,再创建 services 目录。而不是通过 META-INF.services 直接创建两个目录,否则可能会出现找不到 SymbolProcessorProvider 的问题。
使用ksp模块
完成上面的步骤后,我们就可以使用我们创建的 ksp 模块了。首先我们需要在app 的 build.gradle 中添加依赖,代码示例如下所示:
plugins {
// 应用ksp插件
id("com.google.devtools.ksp") version "1.9.0-1.0.11"
}
dependencies {
// 依赖 ksp 模块
implementation(project(":kspLib"))
ksp(project(":kspLib"))
}
在代码中我们就可以使用 @TestFind
注解,代码示例如下:
class TestClass {
@TestFind
fun testMethod(): Unit {
}
}
构建执行完成后,就可以在 build/generated/ksp 目录下看到ksp 生成的结果了。如下图所示: