Akka分布式游戏后端开发9 基于KSP的Kryo依赖注册方案

50 阅读3分钟

本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。

在数据加载与存库一篇中,我们介绍了 Tracer 用于标记脏数据,这个过程涉及到使用 Kryo 进行序列化,还有将脏数据通过异步线程写入数据库涉及到数据的深拷贝,也是使用的 Kryo。这就涉及到在使用 Kryo 时,我们需要知道 Entity 依赖的所有数据类型,提前注册。

由于在开发中,更改 Entity 相关的数据结构是非常频繁的,所以我们不希望通过运行额外的程序来生成相关注册代码,我们希望这个流程整合到编译流程中,在编译之前先生成这部分注册代码,然后这部分代码跟着现有的代码一起进行编译。这里我们使用 KSP 来进行。

配置

根据 KSP 的规范,我们将模块做如下设置,并引入必要依赖:

processor
├─ .gitignore
├─ build.gradle.kts
└─ src
       ├─ main
       │    ├─ java
       │    ├─ kotlin
       │    │    └─ com
       │    │           └─ mikai233
       │    │                  └─ processor
       │    │                         ├─ EntityDepsProcessor.kt
       │    │                         └─ EntityDepsProcessorProvider.kt
       │    └─ resources
       │           └─ META-INF
       │                  └─ services
       │                         └─ com.google.devtools.ksp.processing.SymbolProcessorProvider
       └─ test
              ├─ java
              ├─ kotlin
              └─ resources

依赖

对于代码生成部分,我们仍然使用 KotlinPoet。

dependencies {
    testImplementation(platform(test.junit.bom))
    testImplementation(test.junit.jupiter)
    implementation(tool.symbol.processing.api)
    implementation(tool.kotlinpoet)
    implementation(tool.kotlinpoet.ksp)
}

同时将 KSP 生成目录标记为源码目录

if (project.name == "common") {
    sourceSets.main {
        kotlin.srcDir("build/generated/ksp/main/kotlin")
    }
    sourceSets.test {
        kotlin.srcDir("build/generated/ksp/test/kotlin")
    }
}

获取 Entity 文件

override fun process(resolver: Resolver): List<KSAnnotated> {
    resolver.getAllFiles().filter { it.packageName.asString().startsWith("com.mikai233.common.entity") }
        .forEach { ksFile ->
            sources.add(ksFile)
            val entityKSClassDeclarations =
                ksFile.declarations.filterIsInstance<KSClassDeclaration>().filter { ksClassDeclaration ->
                    ksClassDeclaration.isSubclassOfInterface("com.mikai233.common.db.Entity")
                }
            entityKSClassDeclarations.forEach { ksClassDeclaration ->
                collectKSClassDeclaration(ksClassDeclaration)
            }
        }
    return emptyList()
}

这段代码也比较好理解,我们从所有代码文件中过滤出指定包下面的文件,然后再过滤出含有 Entity 接口的类。

递归遍历 Entity 获取类型

为了防止重复遍历已经遍历过的类型,所以需要做一个集合,把已经遍历过的类型加进去,下次遇到时直接跳过。

private val ksClassDeclarations = mutableSetOf<KSClassDeclaration>()
private val visitedDeclarations = mutableSetOf<KSClassDeclaration>()

ksClassDeclarations 是最后我们需要的类型

接下来就是做类型遍历了,代码里都有注释,就不做解释了。

// 递归方法收集字段类型
private fun collectKSType(kType: KSType) {
    // 如果是Kryo支持的类型,直接返回
    if (isKryoSupportType(kType)) {
        return
    }

    // 如果是泛型类型,处理泛型
    if (kType.arguments.isNotEmpty()) {
        kType.arguments.forEach { argument ->
            // 递归查找泛型的类型
            argument.type?.resolve()?.let { type ->
                collectKSType(type)
            }
        }
    }

    val declaration = kType.declaration
    // 如果是类类型,递归查找类的字段
    if (declaration is KSClassDeclaration && declaration !in visitedDeclarations) {
        visitedDeclarations.add(declaration)
        collectKSClassDeclaration(declaration)
    } else if (declaration is KSTypeAlias) {
        val ksTypeAlias = kType.declaration as KSTypeAlias
        collectKSType(ksTypeAlias.type.resolve())
    }
}

private fun collectKSClassDeclaration(
    ksClassDeclaration: KSClassDeclaration,
) {
    if (!ksClassDeclaration.isAbstract()) {
        if (!ksClassDeclaration.isPublic()) {
            error("Class declaration ${ksClassDeclaration.qualifiedName?.asString()} is not public, use simple type in your entity instead")
        }
        ksClassDeclarations.add(ksClassDeclaration) // 将类本身加入
    }
    val isIterable = ksClassDeclaration.isSubclassOfInterface("kotlin.collections.Iterable")
    val isMap = ksClassDeclaration.isSubclassOfInterface("kotlin.collections.Map")
    if (!(isIterable || isMap)) {
        ksClassDeclaration.getDeclaredProperties().forEach { ksProperty ->
            // 如果字段有 @Transient 注解,跳过该字段
            if (ksProperty.annotations.any { it.shortName.asString() == "Transient" }) {
                return@forEach
            }
            ksProperty.type.resolve().let { propertyType ->
                collectKSType(propertyType)
            }
        }
    }
}

结果展示

代码生成部分的代码就不展示了,这部分完成后,我们进行编译时,就会自动生成如下代码:

package com.mikai233.common.entity

import kotlin.Array
import kotlin.reflect.KClass

public val EntityDeps: Array<KClass<*>> = arrayOf<Array<KClass<*>>>(
    EntityDeps0
    ).flatten().toTypedArray()

为了防止 Array 过大造成无法编译的情况,还需要限制 Array 里面元素的数量,当元素数量过多时,进行切分,最后合并到一起。

package com.mikai233.common.entity

import java.util.HashMap
import kotlin.Array
import kotlin.reflect.KClass

public val EntityDeps0: Array<KClass<*>> = arrayOf(
    DirectObj::class,
    Player::class,
    PlayerAbstract::class,
    PlayerAction::class,
    Room::class,
    TrackChild::class,
    WorkerId::class,
    WorldAction::class,
    HashMap::class
    )

然后在 Kryo 中直接引用此依赖就好了:

package com.mikai233.common.entity

import com.mikai233.common.serde.DepsExtra
import com.mikai233.common.serde.KryoPool

val EntityKryoPool = KryoPool(EntityDeps + DepsExtra)

DepsExtra 是一些常见的类型,例如 HashMapArrayList之类的,因为在 Entity 中如果我们使用集合的接口类型例如 SetMap,迭代数据类型时,是不能拿到具体类型的,所以需要进行预注册。

再比如 Kotlin 中的一些 emptyListemptyMap这种类型,本身不对外暴露类型,只提供了方法来构造此类型,需要用一种比较 trick 的方式来注册类型。

val DepsExtra = arrayOf<KClass<*>>(
    emptyList<Int>()::class,
    emptyMap<Int, Int>()::class,
    emptySet<Int>()::class,
    listOf(0)::class,
    mapOf(0 to 0)::class,
    setOf(0)::class,
    arrayOf(0)::class,
    Arrays.asList(0)::class,
)

在实际的使用中,应该增加测试用例来单独测试接口类型,防止出现没有覆盖到的情况。当然一般来说 DepsExtra 中的数据已经覆盖到集合接口的常用类型,应该不会出现意外的情况。

结尾

没有结尾。