Kotlin Symbol Processors 的使用

参考文章:

Kotlin Symbol Processing (KSP) Alpha 版现已发布

How to Use Kotlin Symbol Processors on Android

Kotlin 符号处理器,简称 KSP ,功能类似于 KAPT (kotlin 注解处理工具) ,但速度优于 KAPT ,并支持多平台。

为何推出 KSP?

Kotlin 开发者向我们反馈最多的需求就是提高构建速度。许多开发者每天都要迭代并部署数十次应用,所以构建速度缓慢会使开发者不得不将大量时间耗费在等待上。编译 Kotlin 代码的最大挑战之一是 Kotlin 没有原生注释处理系统。诸如 Room 等注释处理器在 Android 平台无处不在,它们依赖于通过 Kotlin 注释处理工具 (KAPT) 实现的 Java 注释处理兼容性。但是,KAPT 的运行速度可能会很慢,因为它需要生成中间的 Java 存根,然后 Java 注释处理系统才能对其进行提取。

在设计 KSP 时,我们考虑了如果从头开始构建,Kotlin 的注释处理应是怎样的形式。KSP 提供了一个功能强大且简单的 API,它可以直接解析 Kotlin 代码,因此大大降低了 KAPT 生成存根所带来的构建速度负担。实际上,利用 Room 库执行的初始基准测试表明,KSP 相比 KAPT 速度提高了 2 倍左右。

使用

在这篇文章中,我们将会快速地深入了解通过 KSP 自动地生成样板代码,并讨论存在的问题和解决方案。

让我们通过回答一个问题开始。什么是注解?

Java 注解用于给你的 Java 代码提供原数据。Java 注解并不直接影响你的代码执行,尽管一些注解类型可用于这种目的。

无需关注使用的平台,只要使用 Kotlin ,你就可能在你的项目中实现 KSP 。

注意注解处理器是一个编译期操作并且不涉及运行时是重要的。

通过一个示例我们将更好的理解 KSP 。

思考下面的 data class :

data class Person(
    val name: String,
    val age: Int?,
    val email: String?,
    val contact: Pair<String, String>?,
)

我嗯想要生成一个 Builder 类,基于 Builder 模式:

class PersonBuilder(name: kotlin.String) {

    private val person: Person = Person(
        name = name,
        age = null,
        email = null,
        contact = null,
    )

    fun age(age: kotlin.Int): PersonBuilder {
        person.age = age
        return this
    }

    fun email(email: kotlin.String): PersonBuilder {
        person.email = email
        return this
    }

    fun contact(contact: kotlin.Pair<kotlin.String, kotlin.String>): PersonBuilder {
        person.contact = contact
        return this
    }

    fun build(): Person = person

}

PersonBuilder 类会在编译期间报错。因为我们不能更新 data class 的属性,它们是 val ,不可修改的。我们必须自动生成一个带有多个属性的对象来修复这个问题。

internal class MutablePerson(
    var name: kotlin.String,
    var age: kotlin.Int?,
    var email: kotlin.String?,
    var contact: kotlin.Pair<kotlin.String, kotlin.String, >?,
) {
    fun toPerson(): Person = Person(
        name = name,
        age = age,
        email = email,
        contact = contact,
    )
}

通过这个内部的用来处理类型转换的类来让 Builder 支持生成 Person 类。然后修改 PersonBuilder 代码:

class PersonBuilder(name: kotlin.String) {

    private val mutablePerson: MutablePerson = MutablePerson(
        name = name,
        age = null,
        email = null,
        contact = null
    )

    fun age(age: kotlin.Int): PersonBuilder {
        mutablePerson.age = age
        return this
    }

    fun email(email: kotlin.String): PersonBuilder {
        mutablePerson.email = email
        return this
    }

    fun contact(contact: kotlin.Pair<kotlin.String, kotlin.String>): PersonBuilder {
        mutablePerson.contact = contact
        return this
    }

    fun build(): Person = mutablePerson.toPerson()

}

实现

我们需要三个 module:

  1. Annotations 模块:定义注解
  2. Processor 模块:生成代码
  3. App 模块:使用注解

Annotations 模块

我们应该创建一个 Java 或 Kotlin Library 模块,创建注解类,定义参数并且通过 @Target 指定注解的对象类型。

创建 annotations module ,并且创建下面两个类:

@Target(AnnotationTarget.CLASS)
annotation class AutoBuilder(val flexible: Boolean)
@Target(AnnotationTarget.PROPERTY)
annotation class BuilderProperty

AutoBuilder 指定数据类型。BuilderProperty 指定应该有独立的方法的对象。最后, filexible 决定是否需要生成作为中介的可变类型的对象。

将数据类添加注解:

@AutoBuilder(flexible = true)
data class Person(
    val name: String,
    @BuilderProperty val age: Int?,
    @BuilderProperty val email: String?,
    @BuilderProperty val contact: Pair<String, String>?
)

Processor 模块

首先需要在项目的 project/build.gradle 文件中添加依赖:

buildscript {
    dependencies {
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10'
    }
}

并且启用插件:

plugins {
  // ...
    id 'com.google.devtools.ksp' version '1.7.0-1.0.6' apply false
}

然后将下面的依赖添加到需要使用 Processor 模块下的 build.gradle 文件中:

implementation project(path: ':annotations')
implementation "com.google.devtools.ksp:symbol-processing-api:1.7.0-1.0.6"

为了处理注解,我们需要一个提供注解处理器的对象,因此,我们需要创建一个类,实现 SymbolProcessorProvider 接口

class AutoBuilderProcessorProviderSymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        TODO("Not yet implemented")
    }
}

下一步是注册这个 provider ,需要先创建下面的目录:

processor/src/main/resources/META-INF/services

并且创建一个下面命名的文件:

com.google.devtools.ksp.processing.SymbolProcessorProvider

在文件中写下上面自定义的 Provider 的全名。例如:

com.chunyu.AutoBuilderProcessorProvider

我们的 provider 必须有一个 SymbolProcessor。因此,创建一个类并实现 SymbolProcessor 接口:

class AutoBuilderSymbolProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger,
    private val options: Map<String, String>
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        TODO("Not yet implemented")
    }
}

然后实现 AutoBuilderProcessorProvider 的 create 方法:

class AutoBuilderProcessorProviderSymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return AutoBuilderSymbolProcessor(
            codeGenerator = environment.codeGenerator,
            logger = environment.logger,
            options = environment.options
        )
    }
}

现在我们应该处理 SymbolProcessor 中的最后的一个 TODO ,我们应该在这个类中实现 process 方法。第一步,我们必须找到带有注释的对象:

val symbols: Sequence<KSClassDeclaration> = resolver
    .getSymbolsWithAnnotation(AutoBuilder::class.java.name)
    .filterIsInstance<KSClassDeclaration>()

注意:process 函数返回一个处理注解的列表来避免重复处理

在开始处理对象之前,有必要检查是否有带有我们指定注释的对象。由于这里返回的对象是一个 Sequence ,我们将使用 hasNext 方法进行过滤:

if (symbols.iterator().hasNext().not()) return emptyList()

注意:对象可以有多个注解,注解可以有各种参数,所以我们需要用它们的名字来获取具体的注解和具体的参数。

为了使代码更具可读性,我使用了以下扩展函数来处理 Sequence 和集合的一些操作:

// return one annotation object from a list of annotations or throw error
fun Sequence<KSAnnotation>.getAnnotation(target: String): KSAnnotation {
    return getAnnotationIfExist(target) ?:
    throw NoSuchElementException("Sequence contains no element matching the predicate.")
}

// return null or one annotation object from a list of annotations
fun Sequence<KSAnnotation>.getAnnotationIfExist(target: String): KSAnnotation? {
    for (element in thisif (element.shortName.asString() == target) return element
    return null
}

// return true if an object has a specific annotation
fun Sequence<KSAnnotation>.hasAnnotation(target: String)Boolean {
    for (element in thisif (element.shortName.asString() == target) return true
    return false
}

// return the parameter's value from an annotation object or throw error
fun <T> List<KSValueArgument>.getParameterValue(target: String): T {
    return getParameterValueIfExist(target) ?:
    throw NoSuchElementException("Sequence contains no element matching the predicate.")
}

// return null or the parameter's value from an annotation object
fun <T> List<KSValueArgument>.getParameterValueIfExist(target: String): T? {
    for (element in thisif (element.name?.asString() == target) (element.value as? T)?.let { return it }
    return null
}

// return true if a particular modifier is included in a list
fun Collection<Modifier>.containsIgnoreCase(name: String)Boolean {
    return stream().anyMatch { it.name.equals(name, true) }
}

现在我们可以使用 forEach 直接遍历 Sequence 处理每一个 KSClassDeclaration 对象:

symbols.forEach { symbol ->
   // …
}

首先,我们必须检查对象是否是数据类:

if (symbol.modifiers.containsIgnoreCase("data").not()) {
    logger.error("This object is not a data class", symbol)
    return emptyList()
}

我们可以使用以下代码从带有特定的注解中检索参数的值:

val flexible = symbol.annotations
   .getAnnotation(AutoBuilder::class.java.simpleName)
   .arguments.getParameterValue<Boolean>("flexible")

这段代码有问题;我们使用了字符串,这增加了出错的可能性。将注释类更改为以下代码:

@Target(AnnotationTarget.CLASS)
annotation class AutoBuilder(val flexible: Boolean) {
    companion object {
        const val flexible = "flexible"
    }
}

并将之前的代码改成如下代码:

val flexible = symbol.annotations
   .getAnnotation(AutoBuilder::class.java.simpleName)
   .arguments.getParameterValue<Boolean>(AutoBuilder.flexible)

为了处理对象,我们需要实现 KSVisitor 接口,该接口有两种泛型类型:

  • D:它是作为输入发送或在流程步骤中创建的对象。
  • R:这是我们的类将返回的对象。

KSVisitor 有一些实现。根据情况,我们可以使用它们而不是实现 KSVisitor。

我们在这个例子中使用了 KSVisitorVoid,因为我们的输入和输出都是空的。

由于我们的注解目标是类,我们必须重写 visitClassDeclaration。

class AutoBuilderVisitor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger,
    private val options: Map<String, String>,
    private val flexible: Boolean,
) : KSVisitorVoid() {

    override fun visitClassDeclaration(classDeclaration: KSClassDeclarationdataUnit) {
        TODO("Not yet implemented")
    }
}

在 visitClassDeclaration 中,我们必须创建我们自动生成的文件:

val file: OutputStream = codeGenerator.createNewFile(
   dependencies = Dependencies(false), 
   packageName = [Package Name],
   fileName = [File Name]
)

为防止重新处理和重新生成不必要的文件,KSP 使用增量处理技术。为简洁起见,我将跳过该技术的工作原理。但是,如果您愿意,可以在文档中阅读有关它的信息。

https://kotlinlang.org/docs/ksp-incremental.html

下面的操作函数帮助我们提高代码的可读性。

operator fun OutputStream.plusAssign(str: String) = write(str.toByteArray())

这是完整的生成 class 代码,需要注意的是在结束时调用 OutputStream 对象的 close 方法 :


private lateinit var file: OutputStream

override fun visitClassDeclaration(classDeclaration: KSClassDeclarationdataUnit) {
    val packageName = classDeclaration.packageName.asString()
    val className = classDeclaration.simpleName.asString()
    val objectName = if (flexible) "mutable$className" else className.replaceFirstChar { c -> c.lowercase() }
    val fileName = "${className}Builder"
    val targetName = if (flexible) "Mutable$className" else className
    file = codeGenerator.createNewFile(
        dependencies = Dependencies(false),
        packageName = packageName,
        fileName = fileName
    )
    file += "package $packageNamenn"
    file += "class $fileName(n"
    classDeclaration.getAllProperties().forEach {
        visitPropertyDeclarationToCreateConstructor(it)
    }
    file += ") {nn"
    file += "tprivate val $objectName$targetName = $targetName(n"
    classDeclaration.getAllProperties().forEach {
        visitPropertyDeclarationToCreatePrivateVariable(it)
    }
    file += "t)nn"
    classDeclaration.getAllProperties().forEach {
        visitPropertyDeclarationToCreateBuilderFunctions(it, fileName, objectName)
    }
    file += "tfun build(): $className = $objectName"
    file += if (flexible) ".to$className()n"
    else "n"
    file += "n}"
    file.close()
}

在这个方法中,需要逐行写入代码。

在这个例子中,会访问三次属性。

第一次是用来创建类的构造器:

class PersonBuilder(name: kotlin.String)
private fun visitPropertyDeclarationToCreateConstructor(property: KSPropertyDeclaration) {
    val typeResolve = property.type.resolve()
    if (property.annotations.hasAnnotation(BuilderProperty::class.java.simpleName).not()) {
        val name: String = property.simpleName.asString()
        val type: String = typeResolve.declaration.qualifiedName?.asString() ?: ""
        val nullable = if (typeResolve.nullability == Nullability.NULLABLE) "?" else ""
        val genericArguments: List<KSTypeArgument> = property.type.element?.typeArguments ?: emptyList()
        val generic = visitTypeArguments(genericArguments, logger::error)
        file += "t$name$type$generic$nullable,n"
    } else {
        if (typeResolve.nullability != Nullability.NULLABLE)
            logger.error("BuilderProperties have to be nullable", property)
    }
}

第二次是创建私有属性:

private val mutablePerson: MutablePerson = MutablePerson(
   name = name,
   age = null,
   email = null,
   contact = null,
)
private fun visitPropertyDeclarationToCreatePrivateVariable(property: KSPropertyDeclaration) {
    val name: String = property.simpleName.asString()
    file += (if (property.annotations.hasAnnotation(BuilderProperty::class.java.simpleName).not())
        "tt$name = $name,n"
    else "tt$name = null,n")
}

第三个是创建 Builder 的函数:

fun age(age: kotlin.Int): PersonBuilder {
   mutablePerson.age = age
   return this
}
private fun visitPropertyDeclarationToCreateBuilderFunctions(property: KSPropertyDeclaration, fileName: String, objectName: String) {
    if (property.annotations.hasAnnotation(BuilderProperty::class.java.simpleName)) {
        val name: String = property.simpleName.asString()
        val type: String = property.type.resolve().declaration.qualifiedName?.asString() ?: ""
        val genericArguments: List<KSTypeArgument> = property.type.element?.typeArguments ?: emptyList()
        val generic = visitTypeArguments(genericArguments, logger::error)
        file += "tfun $name($name$type$generic): $fileName {n"
        file += "tt$objectName.$name = $namen"
        file += "ttreturn thisn"
        file += "t}nn"
    }
}

这样就可以自动生成带有 @AutoBuilder 注解的数据类的 Builder 类了。

这里有三点注意事项。

Note 1:

查找所有相关的类并在文件顶部写入是复杂的。为了解决这个问题,我们应该使用我们的类型的限定名称而不是简单的名称。例如:

kotlin.collections.List<kotlin.Boolean>
// 而不是
List<Boolean>

Note 2:

获得泛型类型并不容易。为了处理它,我们应该使用以下递归函数。

private fun visitTypeArguments(typeArguments: List<KSTypeArgument>): String {
    var result = ""
    if (typeArguments.isNotEmpty()) {
        result += "<"
        typeArguments.forEach { arg ->
            result += "${visitTypeArgument(arg)}, "
        }
        result += ">"
    }
    return result
}
private fun visitTypeArgument(typeArgument: KSTypeArgument): String {
    var result = ""
    when (val variance: Variance = typeArgument.variance) {
        Variance.STAR -> result += "*" // <*>
        Variance.COVARIANT, Variance.CONTRAVARIANT -> result += "${variance.label} " // <out ...>, <in ...>
        Variance.INVARIANT -> {} /*Do nothing.*/
    }
    if (result.endsWith("*").not()) {
        val resolvedType = typeArgument.type?.resolve()
        result += resolvedType?.declaration?.qualifiedName?.asString() ?: run {
            logger.error("Invalid type argument", typeArgument)
        }
        // Generating nested generic parameters if any.
        val genericArguments = typeArgument.type?.element?.typeArguments ?: emptyList()
        result += visitTypeArguments(genericArguments)
        // Handling nullability.
        result += if (resolvedType?.nullability == Nullability.NULLABLE) "?" else ""
    }
    return result
}

Note 3:

调用 resolve 操作要花费十分昂贵的运算,所以,你必须小心使用它。

我们的处理模块最终会生成以下代码:

class PersonBuilder(name: kotlin.String) {

    private val mutablePerson: MutablePerson = MutablePerson(
        name = name,
        age = null,
        email = null,
        contact = null,
    )

    fun age(age: kotlin.Int): PersonBuilder {
        mutablePerson.age = age
        return this
    }

    fun email(email: kotlin.String): PersonBuilder {
        mutablePerson.email = email
        return this
    }

    fun contact(contact: kotlin.Pair<kotlin.String, kotlin.String>): PersonBuilder {
        mutablePerson.contact = contact
        return this
    }

    fun build(): Person = mutablePerson.toPerson()
}

App 模块

将 processor 模块添加到 App 模块,首先要在 App 模块下启用插件

plugins {
  // ...
    id 'com.google.devtools.ksp'
}

然后添加下面的依赖:

dependencies {
    implementation project(path: ':annotations')
    ksp project(path: ':processor')
    // ...
}

重新构建并且同步依赖,然后创建 Person 类:

@AutoBuilder(flexible = true)
data class Person(
    val name: String,
    @BuilderProperty val age: Int?,
    @BuilderProperty val email: String?,
    @BuilderProperty val contact: Pair<String, String>?,
)

再次构建项目并在以下地址找到您的文件:

~/build/generated/ksp/debug/kotlin/...

通过命令行查看:

➜  ksp cd app/build/generated/ksp/debug/kotlin/com/chunyu/ksp 
➜  ksp ls
MutablePerson.kt PersonBuilder.kt
➜  ksp 

问题与解决方案

Question 1: 在 Android 模式下展示自动生成的对象。

要在 Android 模式下显示自动生成的对象,我们应该将以下代码添加到 .gradle 文件中。在 app module 下的 build.gradle 文件中添加:

android {
  // ...
    applicationVariants.all { variant ->
        kotlin.sourceSets {
            def name = variant.name
            getByName(name) {
                kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin")
            }
        }
    }
}

如果是要在 android library 中,则是:

libraryVariants.all { variant ->
    kotlin.sourceSets {
        def name = variant.name
        getByName(name) {
            kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin")
        }
    }
}

Question 2: 增加可配置文件

我们可以在 ksp 块中编写我们的配置以获得配置选项。

android {
    // ...
    ksp {
        arg("myConfig1""true")
        arg("myConfig2""myText")
        arg("myConfig3""1")
    }
}

使用以下代码来读取配置。

class Options {
    companion object {
        const val myConfig1 = "myConfig1"
        const val myConfig2 = "myConfig2"
        const val myConfig3 = "myConfig3"

        fun isActive(options: Map<String, String>, option: String) = options[option] == "true"
        fun getText(options: Map<String, String>, option: String) = options[option]
        fun <T> getValue(options: Map<String, String>, option: String) : T? = options[option] as? T
    }
}



原文始发于微信公众号(八千里路山与海):Kotlin Symbol Processors 的使用

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/85068.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!