前幾天寫了一篇,關於利用 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
註解。就這樣吧,有問題在推文文末留言。
本文對你有幫助嗎?留言、點贊、轉發是最大的支持,謝謝!
公衆號後臺回覆成長『成長』,將會獲得我準備的學習資料,也能回覆『加羣』,一塊兒學習進步;你還能回覆『提問』,向我發起提問。