概述
KSP (Kotlin Symbol Processing)是以 Kotlin 优先的 kapt 替代方案。KSP 可直接分析 Kotlin 代码,使得速度提高多达 2 倍。此外,它还可以更好地了解 Kotlin 的语言结构。
注意:ksp和kapt(apt)一样,都只能生成新代码文件,无法修改源代码
接下来通过实践,看看ksp api是如何使用的。
具体代码见 github.com/ccnio/Wareh...
ksp api 使用
功能:对一个使用了 ExtractorInterface 注解的类生成这个类的接口。
kotlin
@ExtractorInterface("IBClass")
class BClass {
fun funB() {
}
}
// 生成 IBClass.kt
public interface IBClass {
public fun funB(): Unit
}
- 创建一个 kotlin libray,build.gradle.kts 配置如下:
scss
plugins {
//org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "org-jetbrains-kotlin-jvm" }
alias(libs.plugins.org.jetbrains.kotlin.jvm)
}
dependencies {
implementation(libs.symbol.processing.api)//引入ksp
implementation(libs.kotlinpoet)
implementation(libs.kotlinpoet.ksp)
}
- 新建 SymbolProcessorProvider、SymbolProcessorEnvironment 并注册
创建TestKsp.kt,修改如下代码:
kotlin
class ExtractInterfaceProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return ExtractInterfaceProcessor(
environment.options,
environment.logger,
environment.codeGenerator
)
}
}
class ExtractInterfaceProcessor(
private val options: Map<String, String>,
private val logger: KSPLogger,
private val codeGenerator: CodeGenerator
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
resolver.getSymbolsWithAnnotation(ExtractorInterface::class.qualifiedName.toString())
.filterIsInstance<KSClassDeclaration>()
.forEach(::generateInterface) // 具体的实现参见 github
return emptyList()
}
}
// 实际开发中 注解相关内容会在单独的一个 library
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ExtractorInterface(val name: String)
注册:创建main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider 路径的文件, 并在文件里配置我们创建的 Processor: com.ccino.ksp.ExtractInterfaceProcessorProvider

- 在app项目中使用我们创建的 ksp
bash
plugins {
//ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
alias(libs.plugins.ksp)
}
dependencies {
implementation(project(":kspDemo")) // 为了能够引入注解
"ksp"(project(":kspDemo")) // 为了生成代码
}
大概步骤就是上面这样,具体还是直接看代码比较直观:github.com/ccnio/Wareh...
给 processor 传参
less
ksp {
arg("parameter", "paramValue")
}
// processor 里获取
environment.options["parameter"]
ksp 依赖关系和增量处理
增量编译用来提升编译速度,再次编译时尽量只处理有更改的文件,默认是开启的。
当 processor 启动后, resolver 法 getSymbolsWithAnnotation、getAllFiles 将仅返回脏文件和文件中的元素:
- Resolver::getAllFiles - 仅返回脏文件引用。
- Resolver::getSymbolsWithAnnotation - 仅返回脏文件中的符号。
对于每个输入文件,一般生成一个或多个输出文件。 当输入文件发生更改时,它就会变脏。 当为其生成相应的输出文件后,输入文件就变得干净了。还有一种情况,生成的文件不仅基于带注释的元素,还基于其父元素。 因此,如果此父级发生更改,则应重新处理该文件。为了确定哪些源需要重新处理,KSP 需要 processor 的帮助来识别哪些输入源对应于哪些输出源。 更具体地说,每当生成新文件时,我们都必须使用 Dependency 指定依赖项。Dependency 允许设置聚合参数,并且可以指定任意数量的文件依赖项。
场景一,指定生成的文件仅依赖于使用了带注解类的文件:
ini
val dependencies = Dependencies(
aggregating = false,
annotatedClass.containingFile!!
)
val file = codeGenerator.createNewFile(
dependencies,
packageName,
fileName
)
场景2,我们想让这个文件也依赖于其他文件,如包含带注释的类的父级的文件,我们需要显式地定义它们:
scss
fun classWithParents(
classDeclaration: KSClassDeclaration
): List<KSClassDeclaration> =
classDeclaration.superTypes
.map { it.resolve().declaration }
.filterIsInstance<KSClassDeclaration>()
.flatMap { classWithParents(it) }
.toList()
.plus(classDeclaration)
val aggregating = annotateClass.superTypes.first().toString() != "Any"
val dependencies = Dependencies(
aggregating = aggregating,
*classWithParents(annotatedClass)
.mapNotNull { it.containingFile }
.toTypedArray()
)
在大多数情况下,我们使用场景一,将聚合设置为 false(将此文件设置为隔离),并且我们依赖于用于生成此输出文件的文件,该文件通常是包含此带注释的元素的文件。(然而实际测试过程中,场景一的情况,修改了父类还是会造成对应文件的修改,不知道是api版本问题,还是我理解错误)。
如果我们的文件生成基于其他文件,我们还应该将它们列为依赖项(场景二)。 依赖于多个其他文件时应设置为聚合,但请记住,当任何文件更改时,聚合文件的依赖关系会变脏,这些变动也是会相互传递的。
ksp 日志无法打印问题
默认情况下 logger.info 是不打印的, logger.warn 及以上级别可以打印。想要打印 info 可以这样配置:
