介面返回的 JSON,再離譜也有辦法,談談 JSON 容錯!

一、序
技術簡歷的技能樹這一項中,JSON 和 GSON 都是常客。但是還有面試候選者將他們的理解停留在最簡單的使用上。
"JSON 是一種具有自描述的、獨立於語言的、輕量級文字資料交換格式,經常被用於資料的儲存和傳輸。而 GSON 可以幫我們快速的將 JSON 資料,在物件之間序列化和反序列化。"
GSON 的 toJson()
和 fromJson()
這兩個方法,是 GSON 最基本的使用方式,它很直觀,也沒什麼好說的。但當被問及 GSON 如何對 JSON 資料容錯,如何靈活序列化和反序列化的時候,就有點抓瞎了。
JSON 資料容錯,最簡單的方式是讓前後端資料保持一致,就根本不存在容錯的問題,但是現實場景中,並不如我們預期的那般美好。
舉兩個簡單的例子:User 類中的姓名,有些介面返回的 Key 值是 name
,而有些返回的是 username
,如何做容錯呢?再比如 age
欄位返回的是如 "18"
這樣的字串,而 Java 物件將其解析成 Int 型別時,雖然 GSON 有一定的型別容錯性,這樣解析能夠成功,但是如果 age
欄位的返回值變成了 ""
呢,如何讓其不丟擲異常,並且設定為預設值 0?
在本文中,我們就來詳細看看,GSON 是如何對資料進行容錯解析的。
二、GSON 的容錯
2.1 GSON 的常規使用
GSON 是 Google 官方出的一個 JSON 解析庫,比較常規的使用方式就是用 toJson()
將 Java 物件序列化成 JSON 資料,或者用 fromJson()
將 JSON 資料反序列化成 Java 物件。
// 序列化 val user = User() user.userName = "Android開發架構" user.age = 18 user.gender = 1 val jsonStr = Gson().toJson(user) Log.i("cxmydev","json:$jsonStr") // json:{"age":18,"gender":1,"userName":"Android開發架構"} // 反序列化 val newUser = Gson().fromJson(jsonStr,User::class.java) Log.i("cxmydev","userName:${newUser.userName}") // userName:Android開發架構
GSON 很方便,大部分時候並不需要我們額外處理什麼,拿來即用。唯一需要注意的可能就是泛型擦除,針對泛型的解析,無非就是引數的差異而已。
在資料都很規範的情況下,使用 GSON 就只涉及到這兩個方法,但是針對一些特殊的場景,就沒那麼簡單了。
免費獲取安卓開發架構的資料(包括Fultter、高階UI、效能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線網際網路公司關於android面試的題目彙總可以加入【安卓開發架構】
2.2 GSON 的註解
GSON 提供了註解的方式,來配置最簡單的靈活性,這裡介紹兩個註解 @SerializedName 和 @Expose。
@SerializedName 可以用來配置 JSON 欄位的名字,最常見的場景來自不同語言的命名方式不統一,有些使用下劃線分割,有些使用駝峰命名法。
還是拿 User 類來舉例,Java 物件中定義的使用者名稱稱,使用的是 userName
,而返回的 JSON 資料中,用的是 user_name
,這樣的差異就可以用 @SerializedName 來解決。
class User{ @SerializedName("user_name") var userName :String? = null var gender = 0 var age = 0 }
而在前文中,針對同一個 User 物件中的使用者名稱稱,現在不同的介面返回有差異,分別為: name
、 user_name
、 username
,這種差異也可以用 @SerializedName 來解決。
在 @SerializedName 中,還有一個 alternate
欄位,可以對同一個欄位配置多個解析名稱。
class User{ @SerializedName(value = "user_name",alternate = arrayOf("name","username")) var userName :String? = null var gender = 0 var age = 0 }
再來看看 @Expose,它是用來配置一些例外的欄位。
在序列化和反序列化的過程中,總有一些欄位是和本地業務相關的,並不需要從 JSON 中序列化出來,也不需要在傳遞 JSON 資料的時候,將其序列化。
這樣的情況用 @Expose 就很好解決。從字面上理解,將這個欄位暴露出去,就是參與序列化和反序列化。而一旦使用 @Expose,那麼常規的 new Gson()
的方式已經不適用了,需要 GsonBuilder
配合 .excludeFieldsWithoutExposeAnnotation()
方法使用。
@Expose 有兩個配置項,分別是 serialize
和 deserialize
,他們用於指定序列化和反序列化是否包含此欄位,預設值均為 True。
class User{ @SerializedName(value = "user_name",alternate = arrayOf("name","username")) @Expose var userName :String? = null @Expose var gender = 0 var age = 0 @Expose(serialize = true,deserialize = false) var genderDesc = "" } fun User.gsonTest(){ // 序列化 val user = User() user.userName = "Android開發架構" user.age = 18 user.gender = 1 user.genderDesc = "男" val gson = GsonBuilder() .excludeFieldsWithoutExposeAnnotation() .create() val jsonStr = gson.toJson(user) Log.i("cxmydev","json:$jsonStr") // json:{"gender":1,"genderDesc":"男","user_name":"承香墨影"} val newUser = gson.fromJson(jsonStr,User::class.java) Log.i("cxmydev","genderDesc:${newUser.genderDesc}") // genderDesc: }
可以看到上面的例子中,genderDesc 用於說明性別的描述,使用 @Expose(serialize = true,deserialize = false)
標記後,這個欄位只參與序列化(serialize = true),而不參與反序列化(deserialize = false)。
需要注意的是,一旦開始使用 @Expose 後,所有的欄位都需要顯式的標記是否“暴露”出來。上例中, age
欄位沒有 @Expose 註解,所以它在序列化和反序列化的時候,均不會存在。
GSON 中的兩個重要的欄位註解,就介紹完了,正確的使用他們可以解決 80% 關於 GSON 解析資料的問題,更靈活的使用方式,就不是註解可以解決的了。
2.3 GsonBuilder 靈活解析
就像前面的例子中看到的一樣,想要構造一個 Gson 物件,有兩種方式, new Gson()
和利用 GsonBuilder
構造。
這兩種方式都可以構造一個 Gson 物件,但是在這個 Builder 物件,還提供一些快捷的方法,方便我們更靈活的解析 JSON。
例如預設情況下,GSON 是不會解析為 null 的欄位的,而我們可以通過 .serializeNulls()
方法,來讓 GSON 序列化為 null 的欄位。
// 序列化 val user = User() user.age = 18 user.gender = 1 val jsonStr = GsonBuilder().create().toJson(user) Log.i("cxmydev","json:$jsonStr") // json:{"age":18,"gender":1} val jsonStr1 = GsonBuilder().serializeNulls().create().toJson(user) Log.i("cxmydev","json1:$jsonStr1") // json1:{"age":18,"gender":1,"userName":null}
GsonBuilder 還提供了更多的操作:
-
.serializeNulls()
:序列化為 null 的欄位。 -
.setDateFormat()
:設定日期格式,例如:setDateFormat("yyyy-MM-dd")
。 -
.disableInnerClassSerialization()
:禁止序列化內部類。 -
.generateNonExcutableJson()
:生成不可直接解析的 JSON,會多)]}'
這 4 個字元。 -
.disableHtmlEscaping()
:禁止轉移 HTML 標籤 -
.setPrettyPrinting()
:格式化輸出
無論是註解還是 GsonBuilder 中提供的一些方法,都是 GSON 針對一些特殊場景下,為我們提供的便捷 API,更復雜一些的場景,就不是它們所能解決的了。
2.4 TypeAdapter
如果前面介紹的規則,都滿足不了業務了,沒關係,Gson 還有大招,就是使用 TypeAdapter。
這裡講的 TypeAdapter 是一個泛指,它雖然確實是一個 GSON 庫中的抽象類,但在 GSON 的使用中,它又不是一個類。
使用 TypeAdapter 就需要用到 GsonBuilder 類中的 registerTypeAdapter()
,我們先來看看這個類的方法實現。
public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) { $Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?> || typeAdapter instanceof JsonDeserializer<?> || typeAdapter instanceof InstanceCreator<?> || typeAdapter instanceof TypeAdapter<?>); if (typeAdapter instanceof InstanceCreator<?>) { instanceCreators.put(type, (InstanceCreator) typeAdapter); } if (typeAdapter instanceof JsonSerializer<?> || typeAdapter instanceof JsonDeserializer<?>) { TypeToken<?> typeToken = TypeToken.get(type); factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter)); } if (typeAdapter instanceof TypeAdapter<?>) { factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter)); } return this; }
可以看到註冊方法,需要制定一個數據型別,並且它除了支援 TypeAdapter 之外,還支援 JsonSerializer 和 JsonDeserializer。InstanceCreator 的使用場景太少了,就不談了。
TypeAdapter(抽象類)、JsonSerializer(介面)、JsonDeserializer(介面) 都可以理解成我們前面說的 TypeAdapter 的泛指,他們具體有什麼區別呢?
TypeAdapter 中包含兩個主要的方法 write()
和 read()
方法,分別用於接管序列化和反序列化。而有時候,我們並不需要處理這兩種情況,例如我們只關心 JSON 是如何反序列化成物件的,那就只需要實現 JsonDeserializer 介面的 deserialize()
方法,反之則實現 JsonSerializer 介面的 serialize()
方法,這讓我們的接管更靈活、更可控。
需要注意的是,TypeAdapter 之所以稱之為大招,是因為它會導致前面介紹的所有配置都失效。但並不是使用了 TypeAdapter 之後,所有的規則都需要我們自己實現。注意看 registerTypeAdapter()
方法的第一個引數是指定了型別的,它只會針對某個具體的型別進行接管。
舉個例子就清楚了,例如前文中提到,當一個 ""
的 JSON 欄位,碰上一個 Int 型別的欄位時,就會導致解析失敗,並丟擲異常。
// 序列化 val user = User() user.age = 18 user.gender = 1 val jsonStr = "{\"gender\":\"\",\"user_name\":\"Android開發架構\"}" val newUser = GsonBuilder().create().fromJson(jsonStr,User::class.java) Log.i("cxmydev","gender:${gender}")
在上面的例子中, gender
欄位應該是一個 Int 值,而 JSON 字串中的 gender
為 ""
,這樣的程式碼,跑起來會拋 JsonSyntaxException: java.lang.NumberFormatException: empty String
異常。
我們實現 JsonDeserializer 介面,來接管反序列化的操作。
class IntegerDefault0Adapter : JsonDeserializer<Int> { override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Int { try { return json!!.getAsInt() } catch (e: NumberFormatException) { return 0 } } }
當轉 Int 出現異常時,返回預設值 0。然後使用 registerTypeAdapter()
方法加入其中。
val newUser = GsonBuilder() .registerTypeAdapter(Int::class.java, IntegerDefault0Adapter()) .create().fromJson(jsonStr,User::class.java) Log.i("cxmydev","gender : ${newUser.gender}") // gender : 0
TypeAdapter 的使用,到這裡就介紹完了,這個大招只要放出來,所有 JSON 解析的問題都不再是問題。TypeAdapter 的適用場景還很多,可以根據具體的需求具體實現,這裡就不再過多介紹了。
另外再補充幾個 TypeAdapter 的細節。
1. registerTypeHierarchyAdapter() 的區別
看看原始碼,細心的朋友應該發現了,註冊 TypeAdapter 的時候,還有 registerTypeHierarchyAdapter()
方法,它和 registerTypeAdapter()
方法有什麼區別呢?
區別就在於,接管的型別類,是否支援繼承。例如前面例子中,我們只接管了 Int 型別,而數字型別還有其他的例如 Long、Float、Double 等並不會命中到。那假如我們註冊的是這些數字型別的父類 Number 呢?使用 registerTypeAdapter()
也不會被命中,因為型別不匹配。
此時就可以使用 registerTypeHierarchyAdapter()
方法來註冊,它是支援繼承的。
2. TypeAdapterFactory 工廠類的使用
使用 registerXxx()
方法可以鏈式呼叫,註冊各種 Adapter。
如果嫌麻煩,還可以使用 TypeAdapterFacetory 這個 Adapter 工廠,配合 registerTypeAdapterFactory()
方法,根據型別來返回不同的 Adapter。
其實只是換個了實現方式,並沒有什麼太大的區別。
3. @JsonAdapter 註解
@JsonAdapter 和前面介紹的 @SerializedName、@Expose 不同,不是作用在欄位上,而是作用在 Java 類上的。
它指定一個“Adapter” 類,可以是 TypeAdapter、JsonSerializer 和 JsonDeserializer 這三個中的一個。
@JsonAdapter 註解只是一個更靈活的配置方式而已,瞭解一下即可。
三、小結時刻
GSON 很好用,但是也是建立在使用正確的基礎上。我見識過一些醜陋的程式碼,例如多欄位場景下,也在 Java 物件中配套寫上多個欄位,再增加一個方法用於返回多個欄位中不會 null 的欄位。又或者為了一個 JSON 資料返回的格式,和後端開發“溝通”一下午規範的問題。
堅持規範當然沒有錯,但是因為別人的問題導致自己的工作無法繼續,就不符合精益思維了。
不抽象,就無法深入思考,我們還是就今天的內容做一個簡單的小結。
-
GSON 可以提供了
toJson()
和fromJson()
兩個簡便的方法序列化和反序列化 JSON 資料。 -
通過註解 @SerializedName 可以解決解析欄位不一致的問題以及多欄位的問題。
-
通過註解 @Expose 可以解決欄位在序列化和反序列化時,欄位排除的問題。
-
GsonBuilder 提供了一些便捷的 API,方便我們解析資料,例如
-
更靈活的解析,使用 TypeAdapter,可以精準定製序列化和反序列化的全過程。
就總結五條吧,多了也記不住。
免費獲取安卓開發架構的資料(包括Fultter、高階UI、效能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線網際網路公司關於android面試的題目彙總可以加入【安卓開發架構】