一. 序
前幾天寫(xiě)了一篇,關(guān)于利用 GSON 在 JSON 序列化和反序列化之間墨榄,數(shù)據(jù)容錯(cuò)的文章玄糟。最簡(jiǎn)單的利用 @SerializedName
注解來(lái)配置多個(gè)不同 JSON Key 值,或者再使用 @Expose
來(lái)配置一些例外的情況袄秩。更復(fù)雜一些的數(shù)據(jù)阵翎,可以使用 TypeAdapter 來(lái)解決,TypeAdapter 可以說(shuō)是一顆 GSON 解析 JSON 的銀彈之剧,所有復(fù)雜數(shù)據(jù)解析以及容錯(cuò)問(wèn)題郭卫,都可以通過(guò)它來(lái)解決。還不了解的可以先看看之前的文章《利用 Gson 做好 JSON 數(shù)據(jù)容錯(cuò)》背稼。
文章評(píng)論里和公眾號(hào)后臺(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<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 中宵荒,首先判斷數(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":""
// "languages":null
// "languages":{}
}
例子的 JSON 中狐树,languages
字段表示當(dāng)前用戶所掌握的語(yǔ)言焙压。當(dāng)語(yǔ)言字段沒(méi)有被設(shè)置的時(shí)候,服務(wù)端返回的數(shù)據(jù)不一致抑钟,如何兼容呢涯曲?
我們?cè)谠镜?User 類中,增加一個(gè) languages 的字段在塔,類型為 ArrayList<String>幻件。
var languages = ArrayList<String>()
在 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<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()
方法,判斷當(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<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()}")
}
在這里我直接使用標(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
注解。
就這樣吧刑桑,有問(wèn)題在推文文末留言氯质。
本文對(duì)你有幫助嗎?留言祠斧、點(diǎn)贊闻察、轉(zhuǎn)發(fā)是最大的支持,謝謝琢锋!
公眾號(hào)后臺(tái)回復(fù)成長(zhǎng)『成長(zhǎng)』辕漂,將會(huì)得到我準(zhǔn)備的學(xué)習(xí)資料,也能回復(fù)『加群』吴超,一起學(xué)習(xí)進(jìn)步钉嘹;你還能回復(fù)『提問(wèn)』,向我發(fā)起提問(wèn)烛芬。