在平常开发的过程中,使用json
传递数据已经非常常用了,在我做安卓程序开发的时候,从接口拿到的数据都是json
格式的,通常我们会选择使用gson
库进行解析,使用起来确实很方便。但有的时候服务端会返回一些带null
的数据,这样我们在解析之后属性的值就变成了null
,这样非常容易导致空指针异常,即使是定义模型的时候规定字段不能为空或为其设置一个默认值都无法解决。在多次与后台同学沟通无果后只能自己想办法把这个坑填上顺便把后台同学杀掉。
为了解决这个问题,有两个方向可以考虑:
- 在解析的时候忽略空值
- 将数据中的空值去掉
第一种方案也有别人做过,核心逻辑就是定义一些TypeAdapter
,在反序列化时对当前字段为空的情况进行处理,但这种方案有两个主要问题:
- 如果要统一封装,只能针对一些基础类型定义对应的适配器,如果是用户自己定义的一些数据模型,就需要用户自己定义对应的适配器。
- 无法保留数据模型中的默认值,通常我们见到的字符串适配器只能在字段为空时返回空字符串,模型中的默认值就会被覆盖。
第二种方案相当于是在解析json
字符串之前把所有的null
都去掉,之后再进行反序列化的时候就没有空值了。没有了空字段的干扰,正常的反序列化流程就不会再给字段赋值为空指针了。确定了解决方案之后就可以开始施工了。
创建JsonDeserializer
我们定义了一个JsonDeserializer
用于在反序列化的时候进行数据处理,为了能够对所有模型的解析进行处理,把泛型指定为了Any
,在GsonAdapter
中通过递归调用searchInObject
方法和searchInArray
方法来遍历全部数据。JsonElement
的isJsonNull
方法可以用来判断空指针,我们就通过这个方法来找到全部的空指针并将其对应的字段删除。通过预处理流程,我们已经得到了没有空指针的数据,最后直接调用原始的gson进行解析即可。
private class GsonAdapter(private val gson: Gson) : JsonDeserializer<Any> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Any {
if (json?.isJsonObject == true) {
val obj = json.asJsonObject
searchInObject(obj)
return gson.fromJson(obj, typeOfT)
}
if (json?.isJsonArray == true) {
val array = json.asJsonArray
searchInArray(array)
return gson.fromJson(array, typeOfT)
}
return gson.fromJson(json, typeOfT)
}
private fun searchInObject(obj: JsonObject) {
val nullKey = ArrayList<String>()
obj.keySet().forEach {
val element = obj.get(it)
when {
element.isJsonNull -> nullKey.add(it)
element.isJsonObject -> searchInObject(element.asJsonObject)
element.isJsonArray -> searchInArray(element.asJsonArray)
}
}
nullKey.forEach { obj.remove(it) }
}
private fun searchInArray(arr: JsonArray) {
val nullIndex = LinkedList<Int>()
arr.forEachIndexed { index, jsonElement ->
when {
jsonElement.isJsonNull -> nullIndex.addFirst(index)
jsonElement.isJsonObject -> searchInObject(jsonElement.asJsonObject)
jsonElement.isJsonArray -> searchInArray(jsonElement.asJsonArray)
}
}
nullIndex.forEach { arr.remove(it) }
}
}
创建TypeAdapterFactory
仅仅定义一个JsonDeserializer
就只能对应一个数据类型,而我们定义的还是Any
类型,这样就只能解析Any
类型对应的数据,所以我们需要定义一个工厂,将所有类型的数据都指向我们的万能适配器。
private class GsonFactory(private val originGson: Gson) : TypeAdapterFactory {
private val adapter = GsonAdapter(originGson)
override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T>? {
return TreeTypeAdapter.newFactoryWithMatchRawType(type, adapter).create(originGson, type)
}
}
使用GsonFactory
在定义Gson
的时候调用registerTypeAdapterFactory
方法将工厂注册到Gson
中,该工厂需要传入一个原始的解析器,用于移除空指针后的数据解析。
val gson = GsonBuilder().registerTypeAdapterFactory(GsonFactory(Gson())).create()
效果测试
定义两个数据模型,是其中一个模型作为另一个模型中某字段的类型,丰富测试数据的样式。
class Book {
var name: String = "Book"
var author: List<String> = listOf()
var price: Double = 0.0
var factory1: PrintFactory = PrintFactory()
var factory2: PrintFactory = PrintFactory()
}
class PrintFactory {
var name: String = "VIP"
var location: String = "China"
var enable: Boolean = true
}
编写测试代码
val content = """
{"name":null,"author":["Mike","Apple","Jack",null,null],"price":56,"factory1":null,"factory2":{"name":"CCC","enable":false}}
""".trimIndent()
val book: Book = AppGson.toObject(content)
Log.i("Test",AppGson.toJson(book))
//输出
//{"author":["Mike","Apple","Jack"],"factory1":{"enable":true,"location":"China","name":"VIP"},"factory2":{"enable":false,"location":"China","name":"CCC"},"name":"Book","price":56.0}
content
模拟了从服务端接收到的数据,其中包含多个null
。AppGson
是我为方便Gson
初始化和使用而封装的工具类。我先是使用AppGson
将数据转换为Book
对象,再通过AppGson
将book
对象转换成json
字符串,通过打印json
字符串就可以验证null
是否被正确处理。
通过下方的数据结果可以看出,原本null
的字段都被替换成了数据模型中的默认值,完美的结果了空指针的问题,而且不用定义各种各样的适配器,同时也保留了数据模型中的默认值。
疑问
通过这一套方案的处理,成功的解决了null对于客户端的影响,避免了更多的后台同学遭遇不幸,但我还是不知道为什么要把null
的值序列化出来,这样做有什么意义。希望了解的同学予以解答。