一文了解 ksp 的使用

738 阅读5分钟

在之前的文章一文了解 apt、 kapt 、 ksp 和 kcp 中,我们介绍了 apt、kapt、KSP 以及 kcp 的区别,这篇文章将介绍 ksp的使用。

创建模块

首先我们需要先创建一个 Java or Kotlin Library 模块,如下图所示:

屏幕截图 2025-01-30 225303.png

创建完模块后,在 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.111.9.0 是指 KSP 插件所依赖的 Kotlin 语言或者相关基础库的版本,而1.0.11 表示是 KSP 插件自身的版本。所有支持的 ksp 版本可以见 Ksp Releases

创建 SymbolProcessorProvider 和 SymbolProcessor

在 Ksp 中,具体的代码生成逻辑是通过 SymbolProcessorProviderSymbolProcessor 来实现的。这里以实现一个 打印使用了 @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)
    }
}

对于代码结构解析,可以看到核心是 SymbolProcessorprocess 方法传递过来的 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

屏幕截图 2025-01-31 001156.png

需要注意:在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 生成的结果了。如下图所示:

屏幕截图 2025-01-30 235628.png

屏幕截图 2025-01-30 235709.png

参考