使用Gson解析data class引發(fā)的一點(diǎn)思考

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ì)象分為四種情況:

  1. 使用我們自定義的 InstanceCreator具垫,可以在初始化時(shí)加入它侈离;
  2. 使用默認(rèn)構(gòu)造器,也就是無(wú)參構(gòu)造函數(shù)筝蚕;
  3. 如果是 Collection 或 Map卦碾,則返回對(duì)應(yīng)的對(duì)象;
  4. 使用 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)兮,吾將上下而求索儒拂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末跪楞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子侣灶,更是在濱河造成了極大的恐慌,老刑警劉巖缕碎,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褥影,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡咏雌,警方通過(guò)查閱死者的電腦和手機(jī)凡怎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赊抖,“玉大人统倒,你說(shuō)我怎么就攤上這事》昭” “怎么了房匆?”我有些...
    開(kāi)封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)报亩。 經(jīng)常有香客問(wèn)我浴鸿,道長(zhǎng),這世上最難降的妖魔是什么弦追? 我笑而不...
    開(kāi)封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任岳链,我火速辦了婚禮,結(jié)果婚禮上劲件,老公的妹妹穿的比我還像新娘掸哑。我一直安慰自己,他們只是感情好零远,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布苗分。 她就那樣靜靜地躺著,像睡著了一般遍烦。 火紅的嫁衣襯著肌膚如雪俭嘁。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天服猪,我揣著相機(jī)與錄音供填,去河邊找鬼拐云。 笑死,一個(gè)胖子當(dāng)著我的面吹牛近她,可吹牛的內(nèi)容都是我干的叉瘩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼粘捎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼薇缅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起攒磨,我...
    開(kāi)封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泳桦,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后娩缰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體灸撰,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年拼坎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浮毯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泰鸡,死狀恐怖债蓝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盛龄,我是刑警寧澤饰迹,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站讯嫂,受9級(jí)特大地震影響蹦锋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜欧芽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一莉掂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧千扔,春花似錦憎妙、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至龙誊,卻和暖如春抚垃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工鹤树, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铣焊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓罕伯,卻偏偏與公主長(zhǎng)得像曲伊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子追他,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345