Gson是Android解析Json的老牌子了蔫敲,它的使用和原理也被大家研究的極其透徹了漏益,可以說(shuō)這是一個(gè)相當(dāng)成熟的庫(kù)。但是伴隨kotlin的普及,有一個(gè)問(wèn)題也越發(fā)明顯地暴露了出來(lái)狼讨。
kotlin里有一個(gè) data class 的概念,倒不是什么“黑科技”的東西柒竞,但是確實(shí)相當(dāng)好用政供,它會(huì)自動(dòng)生成hashcode、equals以及toString等方法朽基,都是對(duì)于一個(gè)bean來(lái)說(shuō)很重要的方法布隔。但是這么好用的東西在和gson一起使用時(shí)就出現(xiàn)了一點(diǎn)意外。讓我們看下邊的例子:
// 定義
data class TestBean(
val name: String,
val age: Int
)
// 數(shù)據(jù)
val json = """
{"name":null,"age":null}
""".trimIndent()
// 解析
val bean = gson.fromJson(json, TestBean::class.java)
// 輸出
TestBean(name=null, age=0)
把json換成 {}
或 {"name":null}
或 {"age":null}
稼虎,甚至 {"age":0}
都不會(huì)影響輸出結(jié)果衅檀。也就是說(shuō),當(dāng)gson解析data class時(shí)霎俩,kotlin的null-safe失效了哀军。
其實(shí)這個(gè)問(wèn)題不是data class造成的沉眶,問(wèn)題主要在null-safe,只是data class和gson打交道最多而已杉适。當(dāng)然也不能怪gson谎倔,誰(shuí)讓gson火起來(lái)的時(shí)候kotlin還沒(méi)多少知名度呢。
追溯問(wèn)題產(chǎn)生原因
遇到問(wèn)題自然要追蹤源碼了猿推,想必很多人這樣試過(guò)片习,最終都會(huì)定位到 ReflectiveTypeAdapterFactory.java 這個(gè)類中。為了節(jié)約大家的時(shí)間蹬叭,這里把相關(guān)的部分貼出來(lái):
public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
// ...
@Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
// ...
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
// ...
return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
// ...
@Override void read(JsonReader reader, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
field.set(value, fieldValue);
}
}
};
}
// ...
public static final class Adapter<T> extends TypeAdapter<T> {
// ...
@Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
}
// ...
return instance;
}
}
}
這里有兩處需要我們關(guān)注藕咏,第一處就是 T instance = constructor.construct();
這個(gè) constructor 是一個(gè) ObjectConstructor 對(duì)象,在 ConstructorConstructor 類里可以找到它的實(shí)現(xiàn):
public final class ConstructorConstructor {
// ...
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();
// first try an instance creator
@SuppressWarnings("unchecked") // types must agree
final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type);
if (typeCreator != null) {
return new ObjectConstructor<T>() {
@Override public T construct() {
return typeCreator.createInstance(type);
}
};
}
// ...
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
return defaultConstructor;
}
ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}
// finally try unsafe
return newUnsafeAllocator(type, rawType);
}
Gson 實(shí)例化對(duì)象分為四種情況:
- 使用我們自定義的 InstanceCreator具垫,可以在初始化時(shí)加入它侈离;
- 使用默認(rèn)構(gòu)造器,也就是無(wú)參構(gòu)造函數(shù)筝蚕;
- 如果是 Collection 或 Map卦碾,則返回對(duì)應(yīng)的對(duì)象;
- 使用 UnSafe起宽。
自定義 InstanceCreator 不現(xiàn)實(shí)洲胖,在這個(gè)問(wèn)題上有多少 data class,就得準(zhǔn)備多少 InstanceCreator坯沪。Collection 或 Map 也排除了绿映,我們要處理的是對(duì)象。也就是說(shuō)只有方式 2 和 4 可用腐晾,我們沒(méi)有提供默認(rèn)構(gòu)造器叉弦,所以 Gson 使用了 UnSafe 這種手段。我們這里不追究 UnSafe 是什么藻糖,只要確認(rèn)使用了 UnSafe淹冰,就會(huì)產(chǎn)生上述結(jié)果就好了,不過(guò)有一句必須注意巨柒,它不會(huì)走我們的構(gòu)造器樱拴。
第二處需要注意的就是為什么 String 被賦值為 null,但 Int 沒(méi)有問(wèn)題洋满?這個(gè)玄機(jī)就在 createBoundField 方法里晶乔,我們?cè)儋N一遍:
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
final Gson context, final Field field, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
// ...
return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
// ...
@Override void read(JsonReader reader, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
// 如果有值,或者不是 Primitive 類型牺勾,就賦值
if (fieldValue != null || !isPrimitive) {
field.set(value, fieldValue);
}
}
};
}
if (fieldValue != null || !isPrimitive)
在這里起了很大作用正罢,像 int、char驻民、boolean 以及對(duì)應(yīng)的包裝類等都屬于基本類型腺怯,條件不成立所以不會(huì)賦值袱饭,但字符串和普通對(duì)象不是基本類型,于是就發(fā)生了一開(kāi)始我們看到的現(xiàn)象呛占。
如何解決
現(xiàn)在是解決問(wèn)題的時(shí)候了,一個(gè)自然的想法是避免 UnSafe懦趋,只要提供默認(rèn)構(gòu)造器即可晾虑。讓我們?cè)囋嚳矗?/p>
data class TestBean2(val name : String = "", val age : Int = 0)
@Test
fun deserializeWithDefaultConstructor() {
val json1 = """
{}
""".trimIndent()
val bean1 = gson.fromJson(json1, TestBean2::class.java)
println(bean1)
val json2 = """
{"name":null}
""".trimIndent()
val bean2 = gson.fromJson(json2, TestBean2::class.java)
println(bean2)
val json3 = """
{"age":null}
""".trimIndent()
val bean3 = gson.fromJson(json3, TestBean2::class.java)
println(bean3)
val json4 = """
{"age":0}
""".trimIndent()
val bean4 = gson.fromJson(json4, TestBean2::class.java)
println(bean4)
}
輸出的結(jié)果是這樣的:
TestBean2(name=, age=0)
TestBean2(name=null, age=0)
TestBean2(name=, age=0)
TestBean2(name=, age=0)
看起來(lái)好了很多,只有 json 返回了 name=null 才會(huì)出現(xiàn)問(wèn)題仅叫,這說(shuō)明我們解決了問(wèn)題一帜篇,但沒(méi)解決問(wèn)題二。gson正確地拿到了對(duì)象诫咱,隨后又把 null 賦值給了 name笙隙,而且是用反射強(qiáng)行賦值的。如何解決問(wèn)題二坎缭,反而成為了關(guān)鍵竟痰。
使用默認(rèn)構(gòu)造器是比較常見(jiàn)的解決方式,但當(dāng)json顯式返回null時(shí)該問(wèn)題依然存在掏呼,所以還需要進(jìn)一步處理坏快。
既然使用默認(rèn)值不管用,那么聲明所有字段為可空 ?
類型就可以很簡(jiǎn)單地規(guī)避這個(gè)問(wèn)題憎夷。但還需要考慮另一個(gè)問(wèn)題:fail-fast莽鸿,也就是快速失敗。Json里的數(shù)據(jù)也許大部分是可空的拾给,但總有幾個(gè)字段是不可空的祥得,這是由業(yè)務(wù)本身決定的,例如一個(gè)用戶的uid明顯不能為空蒋得。而使用可空參數(shù)就可能讓一個(gè)空的uid“混”進(jìn)來(lái)级及,在后續(xù)操作中引發(fā)一連串的錯(cuò)誤。當(dāng)然使用默認(rèn)參數(shù)也有同樣的問(wèn)題窄锅。
在可空參數(shù)的基礎(chǔ)上创千,提供一個(gè)不可空的getter可以有效地避免以上問(wèn)題,例如對(duì)data class做以下處理:
data class TestBean3(
@SerializedName("name")
private val _name: String?,
val age: Int
) {
val name: String
get() = _name ?: "" // 返回默認(rèn)值或者拋出異常
}
這是一個(gè)行之有效的方案入偷,也是我認(rèn)為當(dāng)代碼庫(kù)和gson深度耦合后較好的解決方案追驴,雖不能在解析時(shí)就發(fā)現(xiàn)問(wèn)題,但也比使用之后出問(wèn)題強(qiáng)的多疏之。只不過(guò)這樣一來(lái)比較繁瑣殿雪,二來(lái)每個(gè) bean 都會(huì)比原來(lái)大一些。
除此之外锋爪,square出品的moshi還提供了兩種不同的思路丙曙,一種是使用kotlin-reflection爸业,kotlin的反射和java不太一致,你需要依賴一個(gè)至少2.5M的jar文件亏镰,而且反射的性能肯定差一些扯旷。另一種方案是在編譯時(shí)為每個(gè)data class生成TypeAdapter。要參考這兩種方案索抓,可以查看moshi的源碼钧忽,地址是:moshi。另外逼肯,kotlin官方也提供了自己的解析庫(kù)耸黑,它更考慮了kotlin本身的全部特性,這個(gè)庫(kù)是kotlinx.serialization篮幢。JakeWharton也為之增加了對(duì)應(yīng)的retrofit converter:retrofit2-kotlinx-serialization-converter大刊。
也就是說(shuō),如果可以擺脫gson三椿,使用moshi或serialization在kotlin編程時(shí)可以獲得更好的體驗(yàn)缺菌。若要使用gson,要么按照上述方式使用兩個(gè)變量實(shí)現(xiàn)非空校驗(yàn)赋续,要么參考moshi的做法自己寫一套gson的實(shí)現(xiàn)男翰。(其實(shí)是重復(fù)造輪子了,若無(wú)必要不建議這樣操作==Eβ摇)
我的思考
gson是谷歌出品的解析庫(kù)蛾绎,kotlin又是谷歌力推的開(kāi)發(fā)語(yǔ)言,中間出現(xiàn)這樣的不相容問(wèn)題的確出乎所料鸦列,但作為開(kāi)發(fā)者應(yīng)當(dāng)總有自己的應(yīng)對(duì)之法∽夤冢現(xiàn)在的項(xiàng)目大多使用Retrofit進(jìn)行網(wǎng)絡(luò)請(qǐng)求,json的轉(zhuǎn)換也是通過(guò)ConverterFactory完成的薯嗤,其他需要手動(dòng)解析json的地方也可以作簡(jiǎn)單的封裝顽爹,而不是隨用隨創(chuàng)建Gson對(duì)象,因此項(xiàng)目本身對(duì)gson的依賴并不強(qiáng)烈骆姐。如果項(xiàng)目和gson發(fā)生了深度耦合镜粤,就應(yīng)該考慮下自己寫代碼時(shí)是不是太隨意了一些?
另外一點(diǎn)是將data class全部聲明為可空 ?
類型只能算是一種臨時(shí)方案玻褪,因?yàn)閱?wèn)題的根源在gson不兼容kotlin特性肉渴,而不是data class出現(xiàn)了問(wèn)題。解決問(wèn)題應(yīng)該從根源出發(fā)带射,而不是破壞其余部分的結(jié)構(gòu)同规,造成問(wèn)題范圍擴(kuò)大,也是設(shè)計(jì)代碼時(shí)應(yīng)該遵守的原則之一。
我是飛機(jī)醬券勺,如果您喜歡我的文章绪钥,可以關(guān)注我~
編程之路,道阻且長(zhǎng)关炼。唯程腹,路漫漫其修遠(yuǎn)兮,吾將上下而求索儒拂。