这个项目源自于我在了解一些常见java json序列化库后,对其简单又强大的实现感到好奇(例如gson,通过反射方式可直接将POJO类序列化成json字段)。反观用于minecraft相关的nbt序列化库,库实现基本还停留在创建一堆临时Tag对象,再由码农自己手工把数据赋给自定义的对象中这种原始方式。由此,我萌生了开发个反射式序列化nbt库的想法,参照gson的实现一步一步将此想法落地。同时,将自己的开发过程、设计思路记录下来,方便自己/后人查看。
(注:下面是我开发mc相关时接触的第一个第三方nbt序列化库,比较无语的是当时用这个库时mojang官方更新了新的nbt格式,导致这个枚举Tag类型的库根本无法满足需求也不可扩展==||)
compile 'com.flowpowered:flow-nbt:1.0.0' Invalids大佬在知乎回答中提供的api设计原则。一语中的,简单直接地道明了优秀api的设计思想,因此我才能从纷乱的需求中理清思路,朝着自己真正期望的设计方向走去。
如何设计出一些优雅的API接口呢? - invalid s的回答 - 知乎 https://www.zhihu.com/question/31363461/answer/1921481672
需要一个在java JVM环境上运行的nbt序列库,采用类似于gson反射式序列化的实现方法,从而给使用者提供简单强大的序列化功能。
可扩展性:
对未来可能出现的新nbt格式提供扩展支持,历史上mojang曾新增LongArrayTag格式,因此这种可能性是存在的
提供对各种java对象直接序列化支持,以及当出现新java对象 & 库中现有工具无法序列化时,使用者可以自定义新的序列化方法作为扩展
依赖:该库使用kotlin语言进行开发,目前处于我的另一个项目中的一个子package中,未来考虑将这个序列化库单独剥离出来做成一个library
阅读以下内容需要的前置知识:java,kotlin语言的基本知识,包括对二者泛型的理解,对java反射库的基本理解;对minecraft NBT数据格式的理解;对序列化/反序列化概念的了解。
整个库结构的基本设计:
可以用下面一张图概括:

Nbt序列化库基本结构
各类Java/Kotlin对象:字面意思,各种java,kotlin对象,包括使用者自定义的类型/带泛型的对象等。
Tag类型:一个抽象类,用于定义和相关nbt数据格式对应的Tag类型。
二进制数据:Nbt数据对象
Converter:转换器,将各类java/kotlin对象转为继承自Tag的类型对象的一个接口
Serdes:Serialize/deserialize的简称,用于将Tag对象序列化成nbt二进制格式的一个接口
(注:以下代码皆由kotlin语言写成)
Tag抽象类的声明如下:
// NbtRelatedType:一个标识,表示是某一个Nbt数据结构对应的java/kotlin类型
abstract class Tag<NbtRelatedType:Any> {
abstract val name:String
abstract val value:NbtRelatedType
} Tag抽象类定义非常自由,只接受一个上界为Any的泛型参数,并且需要实现name和value两个成员
Serdes接口的声明如下:
interface Serdes<T:Tag<*>> {
//序列化成二进制数据的方法
fun serialize(tag:T):ByteArray
//反序列化方法
fun deserialize(data:ByteArray, start:Int):Pair<T,Int>
//获取serdes对应的nbt数据类型id,因为是kotlin可以直接声明成变量
//对应java则是方法byte getId()
val id:Byte
} Serdes接口的泛型要求是一个继承自Tag的类型,拥有序列化和反序列化方法,并且提供相对应的nbt数据类型id。注意反序列化方法接受一个start作为指针指向ByteArray,以及反序列化方法返回是一个Pair,这是考虑一段数据可能有不止一个并行的nbt数据,所以反序列化前后都需要一个指针。
Converter接口声明如下:
interface TagConverter<T:Tag<*>> {
// 创建Tag对象
fun <V:Any> createTag(name:String, value:V, typeToken: TypeToken<V>):T?
// 从Tag对象中返回值
fun <V:Any> toValue(tag:T, typeToken: TypeToken<V>):Pair<String, V>?
} 和Serdes接口一样,接受一个继承自Tag的类型泛型,但是两个函数又带了另一个泛型参数,这是因为TagConverter面向的是任意可能的java对象,那么使用者调用converter方法时自然需要指定一个类型对象。一个TagConverter实例通常只能处理某一些类型,那么在面对无法处理的类型时,则需要有一种合适的方法返回结果,最简单的方式就是返回null,所以返回接口声明的结果是nullable的。
typeToken参数则是一个类似于gson的TypeToken对象,用于帮助Converter分析传入的类型能否处理。毕竟jvm的类型擦除导致java系语言的泛型通常只在编译器起效,所以需要类似gson的TypeToken这种略显hacky的方法保留泛型类型信息。关于TypeToken这个类型,以及其如何保留泛型参数的功能介绍会在日后说明。
同样的,typeToken参数限制为和函数泛型参数一样的类型,算是对调用者的一个限制,避免出现传入A类型结果是B类型的情况(当然要是你把Converter的泛型全转成Any或星号投射当我没说)
关于Serdes和Converter不同之处的补充:
Serdes和Converter都算是某种“转换器”,其设计有相似之处又有所不同,原因我所期望的作用域不一样。简单来说就是Converter面向的是很多java类型,并转换成某种特定Tag类型,而Serdes则是处理某种特定Tag类型和二进制nbt类型;前者是一对多的映射关系而后者是一对一。在Serdes中,如果传入了不同的Tag对象是不应该出现的情况,最好的做法应该直接抛出IllegalArgumentException,而不是像Converter一样返回null。同样地,serdes也不需要像Converter一样接受额外的TypeToken参数,因为一个serdes只对应一种Tag类型。
(作者注:整个项目最开始的设计源自于对Tag类型的设计,也是参考 Invalid大佬回答 的指点,把Tag当成了一个类似于网络socket一样的存在,后面又发现Tag的设计似乎“太自由了”,开始考虑怎么限制,中间也走了不少弯路,最后发现并不适合对Tag本身声明进行限制,反而应该是对使用Tag的其他模块进行限制比较好。比如Serdes和Converter,限制二者泛型必须为继承自Tag的对象就行了)
基于以上抽象接口,期望的一种简单使用例子:
val converter = getConverter<Type>() // 获取对应的Converter对象
val serdes = getSerdes<TagType>() // 获取对应的Serdes对象
val javaObj:Type = someJavaObject() // 获取一个任意java实例
// 转成 Tag类型
val tag:TagType = converter.createTag<Type>("name", javaObj, getTypeToken<Type>())
// 转成二进制数据
val binaryData = serdes.serialize(tag)
// 从二进制数据反序列化
// 因为返回的byte数组永远是从0开始填充的数据,所以传入0作为起始点
val deserializedTag = serdes.deserialize(binaryData, 0).first
// 序列化->反序列化后的结果和输入应该相等
assertEquals(tag, deserializedTag) 以上便是整个序列化库最顶层的接口设计。Tag和serdes分离实质上解耦了java里用于表示nbt数据对象的类型,和怎么序列化该类型的实现,一个Tag可以有多个不同的序列化实现,使用者完全可以自行改变要如何序列化一个Tag对象。
在做出这个设计之后,我还惊喜地发现这种分离还使得未来snbt的支持成为可能,只需要声明一个新的支持序列化json的接口即可。这或许也算是合理解耦所带来的强大扩展性吧
Tag接口泛型则提供了最上面所要求的“对未来可能出现的新nbt数据类型提供支持”,假如某天mojang突然想加个新nbt类型用于处理图片什么的,那么想让这个序列化库支持也很简单,继承一个新的的Tag类型即可。
Converter接口则提供了从Tag到各种千奇百怪的java对象的转换映射方式,这个接口算是为满足“可以序列任意java对象”提供了一个前提。
在下一篇我会继续介绍对此接口的一些具体实现(主要是对int,string基本类型的序列化),以及实现过程中碰见的问题&思路