Kotlin 也有json解析問題?
這是我參與11月更文挑戰的第9天,活動詳情檢視:2021最後一次更文挑戰
前言
我們開發用到的最多的json解析工具就是Gson了,毫無疑問它一直兢兢業業的,沒有出現過什麼問題,而且還非常好用,但是,在kotlin中使用,Gson沒有頂住,出現了點小問題,下面總結下問題。
問題一:class欄位預設值失效
所有欄位都有預設值的情況
``` @JsonClass(generateAdapter = true)data class DefaultAll( val name: String = "me", val age: Int = 17) fun testDefaultAll() { val json = """{}""" val p1 = gson.fromJson(json, DefaultAll::class.java) println("gson parse json: $p1") val p2 = moshi.adapter(DefaultAll::class.java).fromJson(json) println("moshi parse json: $p2")}// 結果// gson parse json: DefaultAll(name=me, age=17)// moshi parse json: DefaultAll(name=me, age=17)
```
可以看到這種情況下gson和moshi都沒有問題
部分欄位有預設值
@JsonClass(generateAdapter = true)data class DefaultPart( val name: String = "me", val gender: String = "male", val age: Int)
fun testDefaultPart() { // 這裡必須要有age欄位,moshi為了保持空安全不允許age為null val json = """{"age": 17}""" val p1 = gson.fromJson(json, DefaultPart::class.java) println("gson parse json: $p1") val p2 = moshi.adapter(DefaultPart::class.java).fromJson(json) println("moshi parse json: $p2")}
// 結果// gson parse json: DefaultPart(name=null, gender=null, age=17)// moshi parse json: DefaultPart(name=me, gender=male, age=17)
這種情況下gson忽略了name欄位和gender欄位預設值,給非空型別設定了一個null值,這個就不符合預期了。而moshi則沒有影響。
問題分析: gson丟失預設值原因
Gson反序列化物件時
- 先嚐試獲取無參建構函式
- 失敗則嘗試List、Map等情況的建構函式
- 最後使用Unsafe.newInstance兜底(此兜底不會呼叫建構函式,導致所有物件初始化程式碼不會呼叫)
顯然出現這種情況是因為Gson獲取類的無參建構函式失敗了,所以最後走到了unsafe方案。讓我們來看看Kotlin程式碼對應的java程式碼,一探究竟。AS tools -> kotlin -> show kotlin bytecode可以檢視kotlin編譯後的位元組碼,decompile後可以檢視對應的java程式碼。
所有欄位都有預設值
public final class DefaultAll {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public DefaultAll(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
// $FF: synthetic method
public DefaultAll(String var1, int var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 1) != 0) {
var1 = "me";
}
if ((var3 & 2) != 0) {
var2 = 17;
}
this(var1, var2);
}
public DefaultAll() {
this((String)null, 0, 3, (DefaultConstructorMarker)null);
}
}
可以看到這種情況下該類會生成空參建構函式,但是空參建構函式中並沒有賦值,而是呼叫了synthetic method這個額外生成的輔助建構函式對欄位賦預設值。synthetic method倒數第二個引數是一個int型別,用於標記哪些欄位使用預設值賦值,按欄位宣告順序它們對應的flag值為2^n也就是1 2 4 8....
因為存在空參建構函式而且會賦值預設值,所以這種情況下gson使用正常。
部分欄位有預設值
public final class DefaultPart {
@NotNull private final String name;
@NotNull private final String gender;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getGender() {
return this.gender;
}
public final int getAge() {
return this.age;
}
public DefaultPart(@NotNull String name, @NotNull String gender, int age) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(gender, "gender");
super();
this.name = name;
this.gender = gender;
this.age = age;
}
// $FF: synthetic method
public DefaultPart(String var1, String var2, int var3, int var4, DefaultConstructorMarker
var5) {
// 最低不為0表示第一個預設值欄位在json中無值,需要預設值
if ((var4 & 1) != 0) {
var1 = "me";
}
if ((var4 & 2) != 0) {
var2 = "male";
}
this(var1, var2, var3);
}
}
這種情況下該類並沒有生成空參建構函式,所以gson例項化時使用了Unsafe,自然預設值不生效。實際上只有所有欄位都有預設值時才會生成空參建構函式。
解決方案:
分析了這麼多,避免預設值無效的方法已經顯而易見了
- 定義類時所有欄位都給一個預設值,這樣gson就可以正常工作
- 使用Moshi庫
Moshi庫屬於square公司,最初由Jake Wharton主導,他是kotlin的擁躉,不難推測moshi對Kotlin做了相容,實際上也是這樣。
Moshi序列化/反序列化時根據每個類反射建立對應的JsonAdapter,用它來進行具體操作,同時支援使用annotationProcessor編譯時預先生成各個類的JsonAdapter,空間換時間提升效能。 從它的原始碼可以看出來,它做了2件事: 1. 用一個int記錄(欄位超過32個使用多個int)預設值欄位在將要解析的json中是否存在,從最低位到最高位依次記錄第一個到最後一個預設值欄位在json中是否有key,0表示存在,1表示不存在 1. 判斷是否所有預設欄位在json中都有值,若為true則不用管預設值,直接使用json欄位生成例項,若為false則反射呼叫(synthetic構造器只能夠反射呼叫)synthetic構造器例項化物件,synthetic構造器會根據標誌位為預設值欄位賦值
一言蔽之,Moshi通過遵循Kotlin的機制做到了相容。
問題二: Json中value為null的情況
正常情況下後端返回的Json資料中只應該存在Object型別欄位為null的情況,但是現實很骨感,不乏String型別/list型別丟過來也是null的情況。
- 在Java中,null value會覆蓋掉預設值,使用時get方法中判空就可以了。
- 但是在Kotlin中,如果該欄位宣告為非空型別,使用gson序列化後非空型別欄位會被賦予null值,雖然由於空安全檢查是在編譯器進行不會報異常,但是這明顯非常不符合預期。
- 而Moshi中對這個情況做了處理,非空欄位對應的json value為null時拋JsonDataException,對應的key都不存在時也做同樣處理
這些處理邏輯看起來都很合情合理,但是實際開發中不可預期的null value情況又確實存在,我們也不太可能將所有欄位都宣告為可空型別,那麼將Json中null value自定義解析成預設值或許是一個比較好的方法。
Gson自定義解析替換null value
Gson自定義解析使用TypeAdapterFactory
或者單TypeAdapter
,下面示例將宣告為String和List的欄位通過自定義解析器替換Json中null value為空字串和空list
```
class GsonDefaultAdapterFactory: TypeAdapterFactory {
override fun
/**
* null替換成空List
*/
private fun <T : Any> createCollectionAdapter(
type: TypeToken<T>,
gson: Gson
): TypeAdapter<T>? {
val rawType = type.rawType
if (!Collection::class.java.isAssignableFrom(rawType)) {
return null
}
val elementType: Type = `$Gson$Types`.getCollectionElementType(type.type, rawType)
val elementTypeAdapter: TypeAdapter<Any> =
gson.getAdapter(TypeToken.get(elementType)) as TypeAdapter<Any>
return object : TypeAdapter<Collection<Any>>() {
override fun write(writer: JsonWriter, value: Collection<Any>?) {
writer.beginArray()
value?.forEach {
elementTypeAdapter.write(writer, it)
}
writer.endArray()
}
override fun read(reader: JsonReader): Collection<Any> {
val list = mutableListOf<Any>()
// null替換為空list
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return list
}
reader.beginArray()
while (reader.hasNext()) {
val element = elementTypeAdapter.read(reader)
list.add(element)
}
reader.endArray()
return list
}
} as TypeAdapter<T>
}
/**
* null 替換成空字串
*/
private fun <T : Any> createStringAdapter(): TypeAdapter<T> {
return object : TypeAdapter<String>() {
override fun write(writer: JsonWriter, value: String?) {
if (value == null) {
writer.value("")
} else {
writer.value(value)
}
}
override fun read(reader: JsonReader): String {
// null替換為""
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return ""
}
return reader.nextString()
}
} as TypeAdapter<T>
}
} ```
測試程式碼:
``` val gson: Gson = GsonBuilder() .registerTypeAdapterFactory(GsonDefaultAdapterFactory()) .create()
data class Person(
val name: String,
val friends: List
fun testGsonNullValue() {
// 這裡必須要有age欄位,moshi為了保持空安全不允許age為null
val json = """{"name":null, "friends":null}"""
val p1 = gson.fromJson(json, Person::class.java)
println("gson parse json: $p1")
}
``
執行結果
gson parse json: Person(name=, friends=[])`,符合預期
同樣的 Moshi 庫也可以完全解決Gson這種null值帶來的問題
解決方案:
- 使用Gson給所有定義欄位賦值預設值+自定義解析將不可預期的null值過濾
- 使用Moshi,自定義解析過濾不可預期的null值
總結:
遇到kotlin中json解析的問題,可以參考以上解決方案,希望可以幫到大家。