前几天写了一篇,关于利用 GSON 在 JSON 序列化和反序列化之间,数据容错的文章。最简单的利用 @SerializedName
注解来配置多个不一样 JSON Key 值,或者再使用 @Expose
来配置一些例外的状况。更复杂一些的数据,可使用 TypeAdapter 来解决,TypeAdapter 能够说是一颗 GSON 解析 JSON 的银弹,全部复杂数据解析以及容错问题,均可以经过它来解决。还不了解的能够先看看以前的文章《利用 Gson 作好 JSON 数据容错》。java
文章评论里和公众号后台有一些小伙伴,针对具体数据容错的场景,提出了具体的问题。今天就在这篇文章里统一解答,而且给出解决方案。shell
就像前文中介绍的同样,GSON 已经提供了一些简单的注解,去作数据的容错处理。更复杂的操做,就须要用到 TypeAdapter 了,须要注意的是,一旦上了 TypeAdapter 以后,注解的配置就会失效。数据库
TypeAdapter 是 GSON 2.1 版本开始支持的一个抽象类,用于接管某些类型的序列化和反序列化。TypeAdapter 最重要的两个方法就是 write()
和 read()
,它们分别接管了序列化和反序列化的具体过程。编程
若是想单独接管序列化或反序列化的某一个过程,可使用 JsonSerializer 和 JsonDeserializer 这两个接口,它们组合起来的效果和 TypeAdapter 相似,可是其内部实现是不一样的。json
简单来讲,TypeAdapter 是支持流的,因此它比较省内存,可是使用起来有些不方便。而 JsonSerializer 和 JsonDeserializer 是将数据都读到内存中再进行操做,会比 TypeAdapter 更费内存,可是 API 使用起来更清晰一些。数组
虽然 TypeAdapter 更省内存,可是一般咱们业务接口所使用的那点数据量,所占用的内存其实影响不大,能够忽略不计。网络
由于 TypeAdapter、JsonSerializer 以及 JsonDeserializer 都须要配合 GsonBuilder.registerTypeAdapter()
方法,因此在本文中,此种接管方式,统称为 TypeAdapter 接管。数据结构
对于一些强转有效的类型转换,GSON 自己是有一些默认的容错机制的。好比:将字符串 “18” 转换成 Java 中整型的 18,这是被默认支持的。框架
例如我有一个记录用户信息的 User 类。ide
class User{
var name = ""
var age = 0
override fun toString(): String {
return """ { "name":"${name}", "age":${age} } """.trimIndent()
}
}
复制代码
User 类中包含 name
和 age
两个字段,其中 age
对应的 JSON 类型,能够是 18
也能够是 "18"
,这都是容许的。
{
"name":"承香墨影",
"age":18 // "age":"18"
}
复制代码
那假如服务端说,这个用户没有填年龄的信息,因此直接返回了一个空串 ""
,那这个时候客户端用 Gson 解析就悲剧了。
这固然是服务端的问题,若是数据明确为 Int 类型,那么就算是默认值也应该是 0 或者 -1。
但遇到这样的状况,你还用默认的 GSON 策略去解析,你将获得一个 Crash。
Caused by: com.google.gson.JsonSyntaxException:
- java.lang.NumberFormatException:
-- empty String
复制代码
没有一点意外也没有一点惊喜的 Crash 了,那接下来看看如何解决这样的数据容错问题?
由于这里的场景中,只须要反序列化的操做,因此咱们实现 JsonDeserializer 接口便可,接管的是 Int 类型。直接上例子吧。
class IntDefaut0Adapter : JsonDeserializer<Int> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Int {
if (json?.getAsString().equals("")) {
return 0
}
try {
return json!!.getAsInt()
} catch (e: NumberFormatException) {
return 0
}
}
}
fun intDefault0(){
val jsonStr = """ { "name":"承香墨影", "age":"" } """.trimIndent()
val user = GsonBuilder()
.registerTypeAdapter(
Int::class.java, IntDefaut0Adapter())
.create()
.fromJson<User>(jsonStr,User::class.java)
Log.i("cxmydev","user: ${user.toString()}")
}
复制代码
在 IntDefaut0Adapter 中,首先判断数据字符串是否为空字符串 ""
,若是是则直接返回 0,不然将其按 Int 类型解析。在这个例子中,将整型 0 做为一个异常参数进行处理。
还有一些小伙伴比较关心的,对于 JSONObject 和 JSONArray 兼容的问题。
例如须要返回一个 List,翻译成 JSON 数据就应该是方括号 []
包裹的 JSONArray。可是在列表为空的时候,服务端返回的数据,什么状况都有可能。
{
"name":"承香墨影",
"languages":["EN","CN"] // 理想的数据
// "languages":""
// "languages":null
// "languages":{}
}
复制代码
例子的 JSON 中,languages
字段表示当前用户所掌握的语言。当语言字段没有被设置的时候,服务端返回的数据不一致,如何兼容呢?
咱们在本来的 User 类中,增长一个 languages 的字段,类型为 ArrayList。
var languages = ArrayList<String>()
复制代码
在 Java 中,列表集合都会实现 List 接口,因此咱们在实现 JsonDeserializer 的时候,解析拦截的应该是 List。
在这个状况下,可使用 JsonElement 的 isJsonArray()
方法,判断当前是不是一个合法的 JSONArray 的数组,一旦不正确,就直接返回一个空的集合便可。
class ArraySecurityAdapter:JsonDeserializer<List<*>>{
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): List<*> {
if(json.isJsonArray()){
val newGson = Gson()
return newGson.fromJson(json, typeOfT)
}else{
return Collections.EMPTY_LIST
}
}
}
fun listDefaultEmpty(){
val jsonStr = """ { "name":"承香墨影", "age":"18", "languages":{} } """.trimIndent()
val user = GsonBuilder()
.registerTypeHierarchyAdapter(
List::class.java, ArraySecurityAdapter())
.create()
.fromJson<User>(jsonStr,User::class.java)
Log.i("cxmydev","user: ${user.toString()}")
}
复制代码
其核心就是 isJsonArray()
方法,判断当前是不是一个 JSONArray,若是是,再具体解析便可。到这一步就很灵活了,你能够直接用 Gson 将数据反序列化成一个 List,也能够将经过一个 for 循环将其中的每一项单独反序列化。
须要注意的是,若是依然想用 Gson 来解析,须要从新建立一个新的 Gson 对象,不能够直接复用 JsonDeserializationContext,不然会形成递归调用。
另外还有一个细节,在这个例子中,调用的是 registerTypeHierarchyAdapter()
方法来注册 TypeAdapter,它和咱们前面介绍的 registerTypeAdapter()
有什么区别呢?
一般咱们会根据不一样的场景,选择不一样数据结构实现的集合类,例如 ArrayList 或者 LinkedList。可是 registerTypeAdapter()
方法,要求咱们传递一个明确的类型,也就是说它不支持继承,而 registerTypeHierarchyAdapter()
则能够支持继承。
咱们想用 List 来替代全部的 List 子类,就须要使用 registerTypeHierarchyAdapter()
方法,或者咱们的 Java Bean 中,只使用 List。这两种状况都是能够的。
看到这个小标题,可能会有疑问,保留原 Json 字符串是一个什么状况?获得的 Json 数据,自己就是一个字符串,且挺我细细说来。
举个例子,前面定义的 User 类,须要存到 SQLite 数据库中,语言(languages)字段也是须要存储的。说到 SQLite,固然优先使用一些开源的 ORM 框架了,而很多优秀的 ORM-SQLite 框架,都经过外键的形式支持了一对多的存储。例如一篇文章对应多条评论,一条用户信息对应对应多条语言信息。
这种场景下咱们固然可使用 ORM 框架自己提供的一对多的存储形式。可是若是像如今的例子中,只是简单的存储一些有限的数据,例如用户会的语言(languages),这种简单的有限数据,用外键有一些偏重了。
此时咱们就想,要是能够直接在 SQLite 中存储 languages 字段的 JSON,将其当成一个字符串去存储,是否是就简单了?把一个多级的结构拉平成一级,剩下的只须要扩展出一个反序列化的方法,对业务来讲,这些操做都是透明的。
那拍脑壳想,若是 Gson 有简单的容错,那咱们将这个解析的字段类型定义成 String,是否是就能够作到了?
@SerializedName("languages")
var languageStr = ""
复制代码
很遗憾,这并无办法作到,若是你这样使用,你将获得一个 IllegalStateException 的异常。
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 4 column 18 path $.languages
复制代码
之因此会出现这样的状况,简单来讲,虽然 deserialize()
方法传递的参数都是 JsonElement,可是 JsonElement 只是一个抽象类,最终会根据数据的状况,转换成它的几个实现类的其中之一,这些实现类都是 final class,分别是 JsonObject、JsonArray、JsonPrimitive、JsonNull,这些从命名上就很好理解了,它们表明了不通的 JSON 数据场景,就不过多介绍了。
使用了 Gson 以后,遇到花括号 {}
会生成一个 JsonObject,而字符串则是基本类型的 JsonPrimitive 对象,它们在 Gson 内部的解析流程是不同的,这就形成了 IllegalStateException 异常。
那么接下来看看如何解决这个问题。
既然 TypeAdapter 是 Gson 解析的银弹,找不到解决方案,用它就对了。思路继续是用 JsonDeserializer 来接管解析,这一次将 User 类的整个解析都接管了。
class UserGsonAdapter:JsonDeserializer<User>{
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): User {
var user = User()
if(json.isJsonObject){
val jsonObject = JSONObject(json.asJsonObject.toString())
user.name = jsonObject.optString("name")
user.age = jsonObject.optInt("age")
user.languageStr = jsonObject.optString("languages")
user.languages = ArrayList()
val languageJsonArray = JSONArray(user.languageStr)
for(i in 0 until languageJsonArray.length()){
user.languages.add(languageJsonArray.optString(i))
}
}
return user
}
}
fun userGsonStr(){
val jsonStr = """ { "name":"承香墨影", "age":"18", "languages":["CN","EN"] } """.trimIndent()
val user = GsonBuilder()
.registerTypeAdapter(
User::class.java, UserGsonAdapter())
.create()
.fromJson<User>(jsonStr,User::class.java)
Log.i("cxmydev","user: \n${user.toString()}")
}
复制代码
在这里我直接使用标准 API org.json 包中的类去解析 JSON 数据,固然你也能够经过 Gson 自己提供的一些方法去解析,这里只是提供一个思路而已。
最终 Log 输出的效果以下:
{
"name":"承香墨影",
"age":18,
"languagesJson":["CN","EN"],
"languages size:"2
}
复制代码
在这个例子中,最终解析仍是使用了标准的 JSONObject 和 JSONArray 类,和 Gson 没有任何关系,Gson 只是起到了一个桥接的做用,好像这个例子也没什么实际用处。
不谈场景说应用都是耍流氓,那么若是是使用 Retrofit 呢?Retrofit 能够配置 Gson 作为数据的转换器,在其内部就完成了反序列化的过程。这种状况,配合 Gson 的 TypeAdapter,就不须要咱们在额外的编写解析的代码了,网络请求走一套逻辑便可。
若是以为在构造 Retrofit 的时候,为 Gson 添加 TypeAdapter 有些入侵严重了,能够配合 @JsonAdapter
注解使用。
针对服务端返回数据的容错处理,很大一部分其实都是来自双端没有保证数据一致的问题。而针对开发者来讲,要作到外部数据均不可信的,客户端不信本地读取的数据、不信服务端返回的数据,服务端也不能相信客户端传递的数据。这就是所谓防护式编程。
言归正传,咱们小结一下本文的内容:
registerTypeAdapter()
方法须要制定肯定的数据类型,若是想支持继承,须要使用 registerTypeHierarchyAdapter()
方法。@JsonAdapter
注解。就这样吧,有问题在推文文末留言。
本文对你有帮助吗?留言、点赞、转发是最大的支持,谢谢!
公众号后台回复成长『成长』,将会获得我准备的学习资料,也能回复『加群』,一块儿学习进步;你还能回复『提问』,向我发起提问。