本专栏的项目代码在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
是一些常见的类型,例如 HashMap
,ArrayList
之类的,因为在 Entity
中如果我们使用集合的接口类型例如 Set
,Map
,迭代数据类型时,是不能拿到具体类型的,所以需要进行预注册。
再比如 Kotlin 中的一些 emptyList
,emptyMap
这种类型,本身不对外暴露类型,只提供了方法来构造此类型,需要用一种比较 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
中的数据已经覆盖到集合接口的常用类型,应该不会出现意外的情况。
结尾
没有结尾。