一. 序章
文章評(píng)論里后臺(tái)有一些小伙伴,針對(duì)具體數(shù)據(jù)容錯(cuò)的場(chǎng)景,提出了具體的問(wèn)題。今天就在這篇文章里統(tǒng)一解答,并且給出解決方案。
【資料圖】
二. GSON 數(shù)據(jù)容錯(cuò)實(shí)例
就像前文中介紹的一樣,GSON 已經(jīng)提供了一些簡(jiǎn)單的注解,去做數(shù)據(jù)的容錯(cuò)處理。更復(fù)雜的操作,就需要用到 TypeAdapter 了,需要注意的是,一旦上了 TypeAdapter 之后,注解的配置就會(huì)失效。
2.1 什么是 TypeAdapter
TypeAdapter 是 GSON 2.1 版本開(kāi)始支持的一個(gè)抽象類,用于接管某些類型的序列化和反序列化。TypeAdapter 最重要的兩個(gè)方法就是 write()
和 read()
,它們分別接管了序列化和反序列化的具體過(guò)程。
如果想單獨(dú)接管序列化或反序列化的某一個(gè)過(guò)程,可以使用 JsonSerializer 和 JsonDeserializer 這兩個(gè)接口,它們組合起來(lái)的效果和 TypeAdapter 類似,但是其內(nèi)部實(shí)現(xiàn)是不同的。
簡(jiǎn)單來(lái)說(shuō),TypeAdapter 是支持流的,所以它比較省內(nèi)存,但是使用起來(lái)有些不方便。而 JsonSerializer 和 JsonDeserializer 是將數(shù)據(jù)都讀到內(nèi)存中再進(jìn)行操作,會(huì)比 TypeAdapter 更費(fèi)內(nèi)存,但是 API 使用起來(lái)更清晰一些。
雖然 TypeAdapter 更省內(nèi)存,但是通常我們業(yè)務(wù)接口所使用的那點(diǎn)數(shù)據(jù)量,所占用的內(nèi)存其實(shí)影響不大,可以忽略不計(jì)。
因?yàn)?TypeAdapter、JsonSerializer 以及 JsonDeserializer 都需要配合 GsonBuilder.registerTypeAdapter()
方法,所以在本文中,此種接管方式,統(tǒng)稱為 TypeAdapter 接管。
2.2 空字符串轉(zhuǎn) 0
對(duì)于一些強(qiáng)轉(zhuǎn)有效的類型轉(zhuǎn)換,GSON 本身是有一些默認(rèn)的容錯(cuò)機(jī)制的。比如:將字符串 “18” 轉(zhuǎn)換成 Java 中整型的 18,這是被默認(rèn)支持的。
例如我有一個(gè)記錄用戶信息的 User 類。
class User{ var name = "" var age = 0 override fun toString(): String { return """ { "name":"${name}", "age":${age} } """.trimIndent() }}
User 類中包含 name
和 age
兩個(gè)字段,其中 age
對(duì)應(yīng)的 JSON 類型,可以是 18
也可以是 "18"
,這都是允許的。
{"name":"承香墨影","age":18 // "age":"18"}
那假如服務(wù)端說(shuō),這個(gè)用戶沒(méi)有填年齡的信息,所以直接返回了一個(gè)空串 ""
,那這個(gè)時(shí)候客戶端用 Gson 解析就悲劇了。
這當(dāng)然是服務(wù)端的問(wèn)題,如果數(shù)據(jù)明確為 Int 類型,那么就算是默認(rèn)值也應(yīng)該是 0 或者 -1。
但遇到這樣的情況,你還用默認(rèn)的 GSON 策略去解析,你將得到一個(gè) Crash。
Caused by: com.google.gson.JsonSyntaxException: - java.lang.NumberFormatException: --empty String
沒(méi)有一點(diǎn)意外也沒(méi)有一點(diǎn)驚喜的 Crash 了,那接下來(lái)看看如何解決這樣的數(shù)據(jù)容錯(cuò)問(wèn)題?
因?yàn)檫@里的場(chǎng)景中,只需要反序列化的操作,所以我們實(shí)現(xiàn) JsonDeserializer 接口即可,接管的是 Int 類型。直接上例子吧。
class IntDefaut0Adapter : JsonDeserializer { 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(jsonStr,User::class.java) Log.i("cxmydev","user: ${user.toString()}")}
在 IntDefaut0Adapter 中,首先判斷數(shù)據(jù)字符串是否為空字符串 ""
,如果是則直接返回 0,否則將其按 Int 類型解析。在這個(gè)例子中,將整型 0 作為一個(gè)異常參數(shù)進(jìn)行處理。
2.3 null、[]、List 轉(zhuǎn) List
還有一些小伙伴比較關(guān)心的,對(duì)于 JSONObject 和 JSONArray 兼容的問(wèn)題。
例如需要返回一個(gè) List,翻譯成 JSON 數(shù)據(jù)就應(yīng)該是方括號(hào) []
包裹的 JSONArray。但是在列表為空的時(shí)候,服務(wù)端返回的數(shù)據(jù),什么情況都有可能。
{"name":"承香墨影","languages":["EN","CN"] // 理想的數(shù)據(jù)// "languages":""http:// "languages":null// "languages":{}}
例子的 JSON 中,languages
字段表示當(dāng)前用戶所掌握的語(yǔ)言。當(dāng)語(yǔ)言字段沒(méi)有被設(shè)置的時(shí)候,服務(wù)端返回的數(shù)據(jù)不一致,如何兼容呢?
我們?cè)谠镜?User 類中,增加一個(gè) languages 的字段,類型為 ArrayList。
var languages = ArrayList()
在 Java 中,列表集合都會(huì)實(shí)現(xiàn) List 接口,所以我們?cè)趯?shí)現(xiàn) JsonDeserializer 的時(shí)候,解析攔截的應(yīng)該是 List。
在這個(gè)情況下,可以使用 JsonElement 的 isJsonArray()
方法,判斷當(dāng)前是否是一個(gè)合法的 JSONArray 的數(shù)組,一旦不正確,就直接返回一個(gè)空的集合即可。
class ArraySecurityAdapter:JsonDeserializer>{ 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(jsonStr,User::class.java) Log.i("cxmydev","user: ${user.toString()}")}
其核心就是 isJsonArray()
方法,判斷當(dāng)前是否是一個(gè) JSONArray,如果是,再具體解析即可。到這一步就很靈活了,你可以直接用 Gson 將數(shù)據(jù)反序列化成一個(gè) List,也可以將通過(guò)一個(gè) for 循環(huán)將其中的每一項(xiàng)單獨(dú)反序列化。
需要注意的是,如果依然想用 Gson 來(lái)解析,需要重新創(chuàng)建一個(gè)新的 Gson 對(duì)象,不可以直接復(fù)用 JsonDeserializationContext,否則會(huì)造成遞歸調(diào)用。
另外還有一個(gè)細(xì)節(jié),在這個(gè)例子中,調(diào)用的是 registerTypeHierarchyAdapter()
方法來(lái)注冊(cè) TypeAdapter,它和我們前面介紹的 registerTypeAdapter()
有什么區(qū)別呢?
通常我們會(huì)根據(jù)不同的場(chǎng)景,選擇不同數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的集合類,例如 ArrayList 或者 LinkedList。但是 registerTypeAdapter()
方法,要求我們傳遞一個(gè)明確的類型,也就是說(shuō)它不支持繼承,而 registerTypeHierarchyAdapter()
則可以支持繼承。
我們想用 List 來(lái)替代所有的 List 子類,就需要使用 registerTypeHierarchyAdapter()
方法,或者我們的 Java Bean 中,只使用 List。這兩種情況都是可以的。
2.4 保留原 Json 字符串
看到這個(gè)小標(biāo)題,可能會(huì)有疑問(wèn),保留原 Json 字符串是一個(gè)什么情況?得到的 Json 數(shù)據(jù),本身就是一個(gè)字符串,且挺我細(xì)細(xì)說(shuō)來(lái)。
舉個(gè)例子,前面定義的 User 類,需要存到 SQLite 數(shù)據(jù)庫(kù)中,語(yǔ)言(languages)字段也是需要存儲(chǔ)的。說(shuō)到 SQLite,當(dāng)然優(yōu)先使用一些開(kāi)源的 ORM 框架了,而不少優(yōu)秀的 ORM-SQLite 框架,都通過(guò)外鍵的形式支持了一對(duì)多的存儲(chǔ)。例如一篇文章對(duì)應(yīng)多條評(píng)論,一條用戶信息對(duì)應(yīng)對(duì)應(yīng)多條語(yǔ)言信息。
這種場(chǎng)景下我們當(dāng)然可以使用 ORM 框架本身提供的一對(duì)多的存儲(chǔ)形式。但是如果像現(xiàn)在的例子中,只是簡(jiǎn)單的存儲(chǔ)一些有限的數(shù)據(jù),例如用戶會(huì)的語(yǔ)言(languages),這種簡(jiǎn)單的有限數(shù)據(jù),用外鍵有一些偏重了。
此時(shí)我們就想,要是可以直接在 SQLite 中存儲(chǔ) languages 字段的 JSON,將其當(dāng)成一個(gè)字符串去存儲(chǔ),是不是就簡(jiǎn)單了?把一個(gè)多級(jí)的結(jié)構(gòu)拉平成一級(jí),剩下的只需要擴(kuò)展出一個(gè)反序列化的方法,對(duì)業(yè)務(wù)來(lái)說(shuō),這些操作都是透明的。
那拍腦袋想,如果 Gson 有簡(jiǎn)單的容錯(cuò),那我們將這個(gè)解析的字段類型定義成 String,是不是就可以做到了?
@SerializedName("languages")var languageStr = ""
很遺憾,這并沒(méi)有辦法做到,如果你這樣使用,你將得到一個(gè) IllegalStateException 的異常。
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 4 column 18 path $.languages
之所以會(huì)出現(xiàn)這樣的情況,簡(jiǎn)單來(lái)說(shuō),雖然 deserialize()
方法傳遞的參數(shù)都是 JsonElement,但是 JsonElement 只是一個(gè)抽象類,最終會(huì)根據(jù)數(shù)據(jù)的情況,轉(zhuǎn)換成它的幾個(gè)實(shí)現(xiàn)類的其中之一,這些實(shí)現(xiàn)類都是 final class,分別是 JsonObject、JsonArray、JsonPrimitive、JsonNull,這些從命名上就很好理解了,它們代表了不通的 JSON 數(shù)據(jù)場(chǎng)景,就不過(guò)多介紹了。
使用了 Gson 之后,遇到花括號(hào) {}
會(huì)生成一個(gè) JsonObject,而字符串則是基本類型的 JsonPrimitive 對(duì)象,它們?cè)?Gson 內(nèi)部的解析流程是不一樣的,這就造成了 IllegalStateException 異常。
那么接下來(lái)看看如何解決這個(gè)問(wèn)題。
既然 TypeAdapter 是 Gson 解析的銀彈,找不到解決方案,用它就對(duì)了。思路繼續(xù)是用 JsonDeserializer 來(lái)接管解析,這一次將 User 類的整個(gè)解析都接管了。
class UserGsonAdapter:JsonDeserializer{ 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(jsonStr,User::class.java) Log.i("cxmydev","user: \n${user.toString()}")}
在這里我直接使用標(biāo)準(zhǔn) API org.json 包中的類去解析 JSON 數(shù)據(jù),當(dāng)然你也可以通過(guò) Gson 本身提供的一些方法去解析,這里只是提供一個(gè)思路而已。
最終 Log 輸出的效果如下:
{"name":"承香墨影","age":18,"languagesJson":["CN","EN"],"languages size:"2}
在這個(gè)例子中,最終解析還是使用了標(biāo)準(zhǔn)的 JSONObject 和 JSONArray 類,和 Gson 沒(méi)有任何關(guān)系,Gson 只是起到了一個(gè)橋接的作用,好像這個(gè)例子也沒(méi)什么實(shí)際用處。
不談場(chǎng)景說(shuō)應(yīng)用都是耍流氓,那么如果是使用 Retrofit 呢?Retrofit 可以配置 Gson 做為數(shù)據(jù)的轉(zhuǎn)換器,在其內(nèi)部就完成了反序列化的過(guò)程。這種情況,配合 Gson 的 TypeAdapter,就不需要我們?cè)陬~外的編寫(xiě)解析的代碼了,網(wǎng)絡(luò)請(qǐng)求走一套邏輯即可。
如果覺(jué)得在構(gòu)造 Retrofit 的時(shí)候,為 Gson 添加 TypeAdapter 有些入侵嚴(yán)重了,可以配合 @JsonAdapter
注解使用。
三. 小結(jié)時(shí)刻
針對(duì)服務(wù)端返回?cái)?shù)據(jù)的容錯(cuò)處理,很大一部分其實(shí)都是來(lái)自雙端沒(méi)有保證數(shù)據(jù)一致的問(wèn)題。而針對(duì)開(kāi)發(fā)者來(lái)說(shuō),要做到外部數(shù)據(jù)均不可信的,客戶端不信本地讀取的數(shù)據(jù)、不信服務(wù)端返回的數(shù)據(jù),服務(wù)端也不能相信客戶端傳遞的數(shù)據(jù)。這就是所謂防御式編程。
言歸正傳,我們小結(jié)一下本文的內(nèi)容:
TypeAdapter(包含JsonSerializer、JsonDeserializer) 是 Gson 解析的銀彈,所有 Json 解析的定制化要求都可以通過(guò)它來(lái)實(shí)現(xiàn)。registerTypeAdapter()
方法需要制定確定的數(shù)據(jù)類型,如果想支持繼承,需要使用 registerTypeHierarchyAdapter()
方法。如果數(shù)據(jù)量不大,推薦使用 JsonSerializer 和 JsonDeserializer。針對(duì)整個(gè) Java Bean 的解析接管,可以使用 @JsonAdapter
注解。
標(biāo)簽: