Moshi with Kotlin Json 库—现代化的最佳损友

Moshi 与 Kotlin

Android(Java) 平台已经有许多 Json 库了,包括 Google 推荐的 Gson,广受欢迎的 Jackson,阿里的 FastJson 等,但今天要说的是大名鼎鼎 Square 公司的 Moshi. (什么?没据说过 Square?OkHttp, Retrofit 可都是他的著做)java

轮子已经有不少了,不过自从 Google 将 Kotlin 定为 Android 亲儿子开发语言后,居然包括 Gson 在内的几乎全部 Json 库均不支持,这固然能够理解,由于他们大多数采用了 Java 反射机制,注定难以适配 Kotlin 独有的特性。所以今天要介绍 Moshi —— 一个与 Kotlin 兼容极好的现代化解析库。android

传统 Java Json 库用于 Kotlin 主要产生两个问题:git

  1. 不支持空安全。即便 Kotlin 变量定义为非空,若 Json 为空则解析出 null 而不会抛出异常,直到数据使用时才抛出诡异的 NPE. 为啥说诡异?由于定义非空的变量默认不须要进行空判断,但实际上他是空的,有点挂羊头买狗肉的意思。
  2. 不支持默认参数。Kotlin 的 data class 极大地方便了开发,默认参数的语法更是直呼太爽。惋惜这种现代化写法或遇到两个问题:①默认参数无效。②解析失败由于没有无参构造函数。而解决办法更是使人崩溃:①不要使用默认参数。②给全部形参所有加上默认参数。

所以,若是你已经使用 Kotlin 做为主要语言,Moshi 将会是绝佳的选择。(KT 自带的解析库也能够考虑)github

另外 Moshi 的贡献者也是 Gson 的主要贡献者,由于 Gson 的先天不足且他已经离开了 Google,故开发了 Moshi,具体能够参考他在 Reddit 的回答:Why use Moshi over Gson?json

基础使用

在 Kotlin 中使用 Moshi 有两个方案:①使用 Kotlin 反射。②使用注解生成。由于 Kotlin 反射库高达 2MB,通常咱们采用第二个方案,并且理论上它也比反射效率更高一些,由于大多数工做在编译器就作完了。数组

Moshi 使用一个叫 Adapter 的东西负责序列化与反序列化,每一个 Kotlin 类都对应一个 Adapter,命名规则为 数据类名+JsonAdapter,借助于内置的基本数据类型 Adapter(Int, String 等) 从而实现任意类的解析。显然,咱们须要在 Adapter 内部列出每一个字段并定义解析方法,这是模板化的重复性工做,能够利用注解帮助实现。安全

上面一段看不懂也不要紧,只须要记得在须要序列化(反序列化)的类上加上 @JsonClass(generateAdapter = true) 注解就好了,例如:bash

@JsonClass(generateAdapter = true)
data class Person(
        val name: String,
        val age: Int,
        val sex: Boolean
)
复制代码

补充一下 Adapter 以便理解ide

传统的 Json 库中,每当读取到一个 Json 属性能够利用反射找到 class 中对应的字段(变量)进行赋值。如今咱们抛弃了反射,那么就须要一个手段找到这个变量,解决方案很是粗暴,那就是在编译时就把已知变量所有列出来,没有列出来的就忽略。负责执行这个工做的就是 Adapter.函数

为何不用反射?

①若使用 Java 反射那么没法支持空安全等 Kotlin 特性。②若使用 Kotlin 反射则须要引入一个 2MB 大小的 jar 文件。

以后的使用相似 Gson 很是简单,相比 FastJson 麻烦一丢丢,毕竟要先建立实例。

val json = "..."

val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<Person> = moshi.adapter(Person::class.java)

person: Person = jsonAdapter.fromJson(json)
System.out.println(person)
复制代码

实际使用中能够将 Moshi 做为单例。

Moshi 异常处理也很是规范,它一共只会抛出两种异常:

  1. IOException:读取 Json 过程当中出现 IO 异常,或者 Json 格式错误。
  2. JsonDataException:数据类型不匹配。

若是要解析成集合,须要先用 Types.newParameterizedType() 包装一下:

val personArrayJson: String = "..."
val type: Type = Types.newParameterizedType(List::class.java, Person::class.java)
val adapter: JsonAdapter<List<Person>> = moshi.adapter(type)
val persons: List<Person> = adapter.fromJson(personArrayJson)
复制代码

流式手动解析

基础解析

Adapter 底层其实使用了 JsonReaderJsonWriter 进行(反)序列化,这两个类几乎是从 Gson 抄过来的,API 很是相似,官方原话:Moshi uses the same streaming and binding mechanisms as Gson. If you’re a Gson user you’ll find Moshi works similarly.

对于动态(脏)的 Json 数据,咱们难以预先得知其包含的字段,此时不得不使用流式解析。Moshi 使用 Okio 做为底层,咱们须要经过 Okio 建立数据源来建立 JsonReader,下面是一个解析 Person 数组的例子

val reader = JsonReader.of(Okio.buffer(Okio.source(jsonFile)))

fun readPersonArray(reader: JsonReader): List<Person> {
    val list = mutableListOf<Person>()
    reader.beginArray()
    while (reader.hasNext()) {
        var name = ""
        var age = 0
        var sex = true

        reader.beginObject()
        while (reader.hasNext()) {
            when (reader.nextName()) {
                "name" -> name = reader.nextString()
                "age" -> age = reader.nextInt()
                "sex" -> sex = reader.nextBoolean()
                else -> reader.skipValue()
            }
        }
        reader.endObject()
        val person = Person(name, age, sex)
        list.add(person)
    }
    reader.endArray()
    return list
}
复制代码

selectName() 优化

咱们注意到一些字段名会重复出现(尤为是解析数组时),每当此时 Moshi 不得不进行 UTF8 解码并分配内存。相比之下咱们能够事先准备好有可能出现的字段名,而后直接进行二进制比对,并返回在字段名序列中的下标。

举个栗子🌰:批改做业时每次都把每道题算一遍再比对答案是很低效的,由于咱们已经知道了有哪些题目,不妨先算出正确答案而后直接对比便可。

首先使用 JsonReader.Options.of() 建立字段名称数组,而后使用 reader.selectName() 读取并匹配字段名,优化后代码以下:

val names = JsonReader.Options.of("name", "age", "sex")

fun readPersonArray(reader: JsonReader): List<Person> {
    val list = mutableListOf<Person>()
    reader.beginArray()
    while (reader.hasNext()) {
        var name = ""
        var age = 0
        var sex = true

        reader.beginObject()
        while (reader.hasNext()) {
            when (reader.selectName(names)) {
                0 -> name = reader.nextString()
                1 -> age = reader.nextInt()
                2 -> sex = reader.nextBoolean()
                else -> {
                    reader.skipName()
                    reader.skipValue()
                }
            }
        }
        reader.endObject()
        val person = Person(name, age, sex)
        list.add(person)
    }
    reader.endArray()
    return list
}
复制代码

自定义 Adapter

更多时候,Json 数据格式是已知的,但其值的格式与 Kotlin Class 定义不一样,此时若彻底使用流式 API 解析就太麻烦了,自定义 Adapter 应运而生,经过 Adapter 咱们能够控制 Json 与 class 如何转换。

有趣的是,Adapter 就是一个普通的类,习惯上咱们给类名加上 Adapter 后缀以示区分,但实际上它并不继承自任何父类,也无需实现任何接口。只须要定义两个函数分别用于 Json→Class 与 Class→Json 的转换,并分别加上 @FromJson@ToJson 注解就好了。

Demo

为了演示首先改变一下 Person 的定义:

data class Person(
        val name: String,
        val age: Int,
        val sex: Sex // 将性别换为枚举
)

enum class Sex {
    MALE, FEMALE
}
复制代码

如今 Sex 再也不是基础数据类型 Moshi 没法识别,咱们来建立一个 SexAdapter 帮助 Moshi 在 SexBoolean 之间转换:

class SexAdapter {
    @FromJson
    fun fromJson(value: Boolean): Sex {
        return if (value) Sex.MALE else Sex.FEMALE
    }

    @ToJson
    fun toJson(sex: Sex): Boolean {
        return sex == Sex.MALE
    }
}
复制代码

最后记得注册一下,而后就能成功解析了:

val json = "..."
val moshi = Moshi.Builder().add(SexAdapter()).build()
val person: Person = moshi.adapter(Person::class.java).fromJson(json)
复制代码

使人发指的灵活👍

确定有小伙伴好奇,为何不直接继承某个类或实现某个接口,恰恰用注解的方式定义函数呢?

这一设计一开始确实使人困扰,包括这些函数参数应该填什么、返回值应该是是什么都没有准确的文档。我第一次运行的感受就是“这竟然跑的通?”“它为何不崩溃呢?”🤣 而这些看似繁杂的设计正是 Moshi Adapter 的灵活性精髓所在。

事实上,@FromJson@ToJson 所注释的函数的参数类型或返回值类型是任意的,只要它能被 Moshi 识别便可! 换句话说你能够把 Adapter 当成一个中间步骤,许多 Adapter 组成处理链将数据一步步转化成所需的类型。以上面的 Demo 为例🌰,咱们接受一个 Boolean 类型的参数并返回 Sex(也就是最终所需的类型),那么整个反序列化过程其实有两个 Adapter 依次参与,分别是 JsonAdapter<Boolean>SexAdapter,前者是 Moshi 内置的,后者是咱们自定义并添加到 Moshi 实例的。

假如 Json 源使用 Int 表示 Boolean,那么咱们能够再写一个 BooleanAdapter,接受 Int 类型参数返回 Boolean,而后再经过 SexAdapter 最终获得 Sex.

固然,你也能够直接在 SexAdapter 中接受 Int 参数并返回 Sex. 可是多一个 Adapter 的优势是若还有其余场合须要用到 Boolean,就能够复用这段逻辑了。

What's more! 这还仅仅是 Adapter 的第一类用法,下面还有更丧心病狂的函数签名供食用😝。

自定义 Adapter 中的委托🤝

假设咱们有这样一个 Json 定义:

[
	{
		"type": "person",
		"data": {
			"name": "Bob",
			"age": 23,
			"sex": true
		}
	},
	{
		"type": "job",
		"data": {
			"name": "developer",
			"salary": 20000
		}
	}
]
复制代码

这是一个数组,每一项都有 typedata 两个字段,讨厌的是根据 type 的不一样,data 的类型也不一样。对于这种“脏”数据咱们能够考虑使用流式 API 手动解析,但若 data 类型很复杂或不少怎么办?

咱们想,能不能写一个 Adapter 先判断 type 的值,根据值的不一样再选用不一样的 Adapter 进一步解析,而这些 Adapter 就能够根据 data class 经过 @JsonClass(generateAdapter = true) 注解自动生成了。

这个需求经过已有方式很难解决,它的核心是 “目标类型是半已知的”

  • 已知:直接使用 Adapter 处理。
  • 未知:使用流式 API 手动解析。
  • 半已知:使用流式 API 解析一部分,而后委托给其余 Adapter 处理。

先来看一下 Adapter 中函数的具体签名要求:

@FromJson:

<any access modifier> R fromJson(T value) throws <any>;
<any access modifier> R fromJson(JsonReader jsonReader) throws <any>;
<any access modifier> R fromJson(JsonReader jsonReader, JsonAdapter<any> delegate, <any more delegates>) throws <any>;
复制代码

@ToJson:

<any access modifier> R toJson(T value) throws <any>;
<any access modifier> void toJson(JsonWriter writer, T value) throws <any>;
<any access modifier> void toJson(JsonWriter writer, T value, JsonAdapter<any> delegate, <any more delegates>) throws <any>;
复制代码

上面咱们一直使用的是第一个函数签名,即:接受一个任意类型的(已经支持转换的)参数并返回一个任意类型。如今咱们要使用第三类,即:接受一个 JsonReader(用于流式解析)以及一系列可能用到的 Adapter(用于委托),而后返回一个任意类型。

首先定义一下数据类:

sealed class Item {
    abstract val name: String
}

@JsonClass(generateAdapter = true)
data class Person(
        override val name: String,
        val age: Int,
        val sex: Boolean
) : Item()

@JsonClass(generateAdapter = true)
data class Job(
        override val name: String,
        val salary: Int
) : Item()
复制代码

而后就能够写出 Adapter:

class ItemAdapter {

    private val names = JsonReader.Options.of("type", "data")

    @FromJson
    fun fromJson( reader: JsonReader, person: JsonAdapter<Person>, job: JsonAdapter<Job> ): Item? {
        reader.beginObject()
        // 解析 type 字段
        // peek() 用于得到一个新的 JsonReader 以便重复解析。
        // 由于咱们没法肯定 type 和 data 哪个会先读取到,所以须要利用 peek 先单独取出 type,
        // 而后再开始正式读取。
        var type: String? = null
        val peek = reader.peekJson()
        loop@ while (peek.hasNext()) {
            when (peek.selectName(names)) {
                0 -> {
                    type = peek.nextString()
                    break@loop // 只要找到 type 就好了
                }
                1 -> peek.skipValue()
                else -> {
                    peek.skipName()
                    peek.skipValue()
                }
            }
        }

        // 真正开始解析数据
        var item: Item? = null
        while (reader.hasNext()) {
            when (reader.selectName(names)) {
                0 -> reader.skipValue()
                1 -> when (type) {
                    "person" -> item = person.fromJson(reader)
                    "job" -> item = job.fromJson(reader)
                    else -> reader.skipValue() // 未知 type,跳过
                }
                else -> {
                    reader.skipName()
                    reader.skipValue()
                }
            }
        }
        reader.endObject()
        return item
    }

    @ToJson
    fun toJson( writer: JsonWriter, value: Item, person: JsonAdapter<Person>, job: JsonAdapter<Job> ) {
        // begin(end)Object 不能够放在 when 外面,不然若遇到不支持的类型会解析出空对象“{}”
        when (value) {
            is Person -> writer.writeItem("person", person, value)
            is Job -> writer.writeItem("job", job, value)
        }
    }

    private fun <T> JsonWriter.writeItem(type: String, adapter: JsonAdapter<T>, data: T) {
        beginObject()
        name("type")
        value(type)
        name("data")
        adapter.toJson(this, data)
        endObject()
    }
}
复制代码

在这个例子中咱们首先经过 JsonReader 手动解析出了 type 字段,而后根据具体的值将后续解析委托给了自动生成的 Adapter. 须要注意的一点是在流式 API 中字段解析的顺序依赖于 Json 字符串,所以有可能先解析出 data,在这种状况下因为缺乏 type 咱们是没法处理这个数据的(不知道委托给谁)。所以咱们要先取出 type 并忽略其余一切字段,可是 Reader 的读取是单向的,若是忽略了先解析出的 data 后续就无法再次取得。因而使用 reader.peekJson() 方法取得一个临时 Reader,它的读取不会影响原先 Reader.

序列化同理,判断具体的数据类型后委托给对应的 Adapter 就好了。

在反序列化时,也许有同窗想先临时保存一下 data 的原始数据,在获得 type 后再进行解析,从而避免使用 peekJson()。问题在于 Moshi 是没有中间层的(例如 Gson 中的 JsonElement),要么解析成一个具体的数据类型,要么忽略,因此这个想法不可行。

参考

相关文章
相关标签/搜索