本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。
在将 Excel 配置表数据解析成可供程序使用的对象之后,我们需要对这个配置表对象进行序列化,以便后续发布到生产环境时,直接反序列化就可以得到此对象。因为我们不可能发布到生产环境时,还带着一堆配置表数据,通常是在打包阶段就会把配置表数据解析好,生成一个二进制文件,最后程序启动的时候,直接解析此二进制文件就可以了。
因此,我们需要一个序列化方案,将 Java 对象序列化以及反序列化。这里我们选用的序列化框架是 Kryo。
Kryo 是一个快速、高效的对象序列化框架,主要用于将 Java 对象序列化为字节流(以及反序列化为对象)。它以性能和紧凑性著称,常用于分布式系统、缓存机制和高效的数据传输场景,例如 Apache Spark、Flink、Akka 等分布式计算框架中。
Kryo 的主要特点:
-
高性能:
- Kryo 的序列化和反序列化速度通常快于 Java 自带的
ObjectOutputStream
/ObjectInputStream
。 - 生成的序列化数据体积较小,占用更少的带宽或存储空间。
- Kryo 的序列化和反序列化速度通常快于 Java 自带的
-
支持多种数据类型:
- Kryo 可以序列化多种 Java 数据类型,包括基本类型、集合、数组、用户自定义类等。
-
可扩展性:
- Kryo 支持注册自定义的序列化器(Serializer),以优化序列化逻辑。
- 用户可以根据需求扩展其序列化机制以支持复杂对象。
-
跨平台兼容性:
- 序列化后的数据格式可以在不同平台上解码,从而实现跨系统数据传输。
-
支持循环引用和嵌套对象:
- Kryo 能够自动检测循环引用,避免递归堆栈溢出问题。
-
轻量级:
- Kryo 体积小,易于嵌入到其他项目中。
GameConfigManager 序列化/反序列化
提供序列化接口
为了使序列化后的文件体积更加紧凑,我们需要抹掉对象的类型信息,使用 id 来代替类型信息,也就是在 Kryo 中使用 register
方法把类型信息关联到某个 id 上,反序列化的时候,直接根据 id 就可以找到类型信息。
abstract class GameConfigs<K : Any, C : GameConfig<K>> : AnalysisEventListener<Map<Int, String?>>(), Map<K, C> {
private var configs: HashMap<K, C> = hashMapOf()
/**
* 将[configs]进行序列化
*/
fun serialize(kryo: Kryo, output: Output) {
kryo.writeObject(output, configs)
}
/**
* 将[configs]进行反序列化
*/
fun deserialize(kryo: Kryo, input: Input) {
@Suppress("UNCHECKED_CAST")
configs = kryo.readObject(input, HashMap::class.java) as HashMap<K, C>
}
}
我们在 GameConfigs
中新增两个接口,用于将里面的 configs
对象序列化和反序列化。之所以要单独把 configs
拿出来序列化,是因为 configs
中的 K
和 V
中含有通配符,在通过反射注册类型时,无法获取到具体的类型,只能把序列化和反序列化的操作延迟到具体的实现类中。在进行反射分析的时候,我们并没有直接注册这个 K
和 V
,而是去看子类的 K
和 V
是什么,再进行注册。
编写序列化工具类
在序列化的过程中,我们先将 GameConfigManager
序列化,然后再序列化里面的 configs
,因为 configs
有 @Transient
注解,所以序列化 GameConfigManager
的时候不会序列化到 configs
。然后向 Kryo 中写入 configs
的数量,迭代整个 configs
,写入每个 GameConfigs
的类型信息以及调用 GameConfigs
的 serialize
进行序列化。反序列化过程和序列化过程相反。
object GameConfigManagerSerde {
fun deserialize(inputStream: InputStream): GameConfigManager =
Input(GZIPInputStream(inputStream)).use { deserialize(it) }
fun deserialize(bytes: ByteArray): GameConfigManager =
Input(GZIPInputStream(bytes.inputStream())).use { deserialize(it) }
fun serialize(manager: GameConfigManager, outputStream: OutputStream) =
Output(GZIPOutputStream(outputStream)).use { serialize(manager, it) }
fun serialize(manager: GameConfigManager): ByteArray {
val bos = ByteArrayOutputStream()
Output(GZIPOutputStream(bos)).use { serialize(manager, it) }
return bos.toByteArray()
}
private fun deserialize(input: Input): GameConfigManager {
return GameConfigKryoPool.use {
val manager = readObject(input, GameConfigManager::class.java)
val size = readObject(input, Int::class.java)
repeat(size) {
@Suppress("UNCHECKED_CAST")
val key = readClass(input).type.kotlin as K
val constructor =
requireNotNull(key.primaryConstructor) { "${key.simpleName} primaryConstructor is null" }
val gameConfigs = constructor.call()
gameConfigs.manager = manager
gameConfigs.deserialize(this, input)
manager.configs[key] = gameConfigs
}
manager
}
}
private fun serialize(manager: GameConfigManager, output: Output) {
GameConfigKryoPool.use {
writeObject(output, manager)
writeObject(output, manager.configs.size)
manager.configs.values.forEach {
writeClass(output, it::class.java)
it.serialize(this, output)
}
}
}
}
GameConfigKryoPool
我们对 Kryo 进行了一下简单封装,提供了一个 Kryo 池来对数据进行序列化和反序列化,因为 Kryo 不是线程安全的对象。然而真正麻烦的不是这里,而是 Kryo 的类注册,因为为了序列化性能和体积,需要进行类注册,Kryo 需要知道序列化对象的所有类型信息,因此我们需要递归的遍历序列化对象中字段的所有类型,提前进行注册。这个操作有一个额外的程序来执行代码生成,因为配置表的变动不频繁,不需要放到编译流程中。
val GameConfigKryoPool = KryoPool(ConfigDeps + ConfigImpl + ConfigsImpl + DepsExtra)
class KryoPool(val classes: Array<KClass<*>>) {
val logger = logger()
private val pool = object : Pool<Kryo>(true, true) {
override fun create(): Kryo {
val kryo = Kryo()
kryo.isRegistrationRequired = true
kryo.instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy())
var id = 30
classes.toSet().forEach {
val nextId = ++id
kryo.register(it.java, nextId)
logger.debug("register class: {} -> {}", it, nextId)
}
return kryo
}
}
init {
//预先创建一些Kryo对象以供使用
val kryoList = mutableListOf<Kryo>()
repeat(100) {
kryoList.add(pool.obtain())
}
kryoList.forEach {
pool.free(it)
}
}
fun <R> use(block: Kryo.() -> R): R {
val kryo = pool.obtain()
return try {
block(kryo)
} finally {
pool.free(kryo)
}
}
}
结尾
以上我们完成的配置表的序列化和反序列化工作,后续会讲配置表代码的生成以及 Kryo 依赖的生成逻辑,这一套模式是不变的,不可能说策划配了一张表后,我还需要手动写一大堆代码,最好的方式就是根据配置表生成配置表代码。