從使用到源碼—Gson(上)

引言

  • 使用Gson已經(jīng)有好長時間了又活,但是一直停留在使用的層面上,因此在對它的好奇心尚未消失之前锰悼,我得跟它做個了斷 柳骄。
  • 關(guān)于Gson,將會有幾篇長篇解讀文箕般,用于記錄自己淺顯的見解耐薯。

使用默認配置的Gson

  • 對于Json數(shù)據(jù)的解析,我們總是希望能夠只是通過input -> output過程就能得到想要的結(jié)果丝里,而不用手動去逐個解析相關(guān)字段曲初,因為太費事了。出于這種需求杯聚,Gson也在盡力配合臼婆,比如以下示例:
    //System.out.println("");
    final String jsonStr = "{'data':'this is data', 'result':{'name':'horseLai', 'age':24}}"; 
    
    Gson gson = new Gson();
     
    //輸出:{"data":"hello gson","result":{"name":"horseLai","age":20}}
    Data data = new Data();
    User user = new User();
    data.setResult(user);
    data.setData("hello gson");
    user.setAge(20);
    user.setName("horseLai");
    String json = gson.toJson(data);
    System.out.println(json);
    
    //輸出:Data{data='this is data', result=User{name='horseLai', age=24}}
    Data data1 = gson.fromJson(jsonStr, Data.class);
    System.out.println(data1 );
    static class Data {
        private String data;
        private User result;
        // 省略set/get/toString
    } 
    static class User {
        private String name;
        private int age; 
        // 省略set/get/toString
    }
  • 那么fromJsontoJson發(fā)生了什么?

    • 我們可以思考一下械媒,如果我們需要從json字符串中分離出字段名及其對應(yīng)的值目锭,然后將這些值填充賦值到Java實體類中對應(yīng)的字段中评汰,我們要做哪些工作纷捞,如何進行?

      • 我的思路
        • 分解json字符串痢虹,直覺是使用正則,但是仔細思考一番會發(fā)現(xiàn)主儡,對于簡單的json數(shù)據(jù)來說奖唯,Ok,但是對象層級深一點的話就復(fù)雜了糜值,有沒有更好的方式呢丰捷?使用棧和Map配合的方式,遇到{[寂汇,標記為對象或數(shù)組的開始病往,然后往棧中壓入數(shù)據(jù),遇到:則表示讀取到了一個字段的名稱骄瓣,接著去除''和一些空白符號后將字段名作為Mapkey停巷,清空棧,接著壓棧榕栏,此時要解析值畔勤,那么可以將' ', '}] '}等作為值的起點或終點,標記扒磁,取值庆揪,最終將字段名和值存到Map中,那么這種方法對于復(fù)雜多層級多類型的json數(shù)據(jù)而言也是OK的妨托,只要標記好是對象還是數(shù)組還是普通數(shù)據(jù)類型缸榛,然后在Map中做好對應(yīng)于Json數(shù)據(jù)的層級即可。如果數(shù)據(jù)是HTML的話兰伤,會比較不好判斷内颗,因為其中包含的符號可能會比較多,干擾判斷医清。
        • 分離了json數(shù)據(jù)中的字段名稱和值起暮,接著填充賦值實體類就容易了,反射通過data.getClass().getField("name").set(xxx)的方式一條龍服務(wù)会烙。
    • 思考過后负懦,帶著疑問與好奇,通過追蹤柏腻,在Gson#fromJson方法中有如下代碼纸厉,可以看出,當(dāng)使用Gson#fromJsonJson數(shù)據(jù)轉(zhuǎn)換到Java實體類時五嫂,會最終調(diào)用TypeAdapter#read方法進行數(shù)據(jù)填充操作颗品。

    public <T> T fromJson(String json, Class<T> classOfT) throws JsonIOException, JsonSyntaxException  {
        // 省略無關(guān)代碼肯尺、調(diào)用層次若干..
    
        TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
        TypeAdapter<T> typeAdapter = getAdapter(typeToken);
        T object = typeAdapter.read(reader);
        
        // . . .   
        return Primitives.wrap(classOfT).cast(object);
    } 
    
    • 而在Gson#fromJson方法中有如下代碼,可以看出躯枢,當(dāng)我們使用Gson#fromJsonJson數(shù)據(jù)轉(zhuǎn)換到我們的Java實體類時则吟,會最終調(diào)用TypeAdapter#write方法進行數(shù)據(jù)填充操作。
    public void toJson(Object src) throws JsonIOException {
        // 省略無關(guān)代碼锄蹂、調(diào)用層次若干..
        // 最終會調(diào)用
      Streams.write(jsonElement, writer);
    }
    
    public static void write(JsonElement element, JsonWriter writer) throws IOException {
        // 實際
        TypeAdapters.JSON_ELEMENT.write(writer, element);
    }
    
    // 注意到它的泛型是JsonElement
    public static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() 
    { // 省略無關(guān)代碼若干..}
    
  • 那么TypeAdapter<T>有什么用氓仲,怎么用?

    • TypeAdapter<T>,類型適配器得糜,包含write``read兩個核心抽象方法敬扛,用于往數(shù)據(jù)流中寫/讀數(shù)據(jù)。
    public abstract class TypeAdapter<T> {
        // ...
         public abstract void write(JsonWriter out, T value) throws IOException;
         public abstract T read(JsonReader in) throws IOException;
    }
    
    • TypeAdapters中包含有它的若干個基本實現(xiàn)類朝抖,比如在Gson#toJson中使用到的TypeAdapters.JSON_ELEMENT啥箭,代碼如下(其中省略了很多操作相同的代碼,感興趣的朋友可以自行查看源碼)治宣,邏輯可看注釋急侥;
      public static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() {
        @Override public JsonElement read(JsonReader in) throws IOException {
          switch (in.peek()) { // 從JsonReader的棧中拿到對應(yīng)的字段類型,接著去匹配炼七、讀取
          case STRING:  
            return new JsonPrimitive(in.nextString());
           //. . .
          case BEGIN_ARRAY:
          // 數(shù)組和對象是比較特殊的缆巧,因為他們有自己的字段、對象或數(shù)組豌拙,
          // 并代表著層級的深度陕悬,因此讀寫時都需要注意標識好
            JsonArray array = new JsonArray();
            in.beginArray(); // 標識數(shù)組起點,告訴它這是個數(shù)組
            while (in.hasNext()) { // 接著通過遞歸讀取按傅、匹配字段類型捉超,并放到數(shù)組中
              array.add(read(in));
            }
            in.endArray(); // 告訴gson,這個數(shù)組已經(jīng)讀完了
            return array;
          // . . .  
        } 
        // 上面分析了從流中讀取并構(gòu)建Json對象的過程唯绍,類似于拆箱操作拼岳,那么寫過程就是裝箱操作了
        @Override public void write(JsonWriter out, JsonElement value) throws IOException {
          if (value == null || value.isJsonNull()) {
            out.nullValue();
          } else if (value.isJsonPrimitive()) { // 寫基本類型,注意String也會歸類到基本類型
            JsonPrimitive primitive = value.getAsJsonPrimitive();
            if (primitive.isNumber()) { 
              out.value(primitive.getAsNumber());
            } //. . . 
          } else if (value.isJsonArray()) { // 寫數(shù)組况芒,跟讀一個道理啦
            out.beginArray(); // 標識起點
            for (JsonElement e : value.getAsJsonArray()) {
              write(out, e); 
            }
            out.endArray();  // 標識終點
          }// . . .
        }
      };
    

定制使用TypeAdapter<T>

  • 上面分析了TypeAdapter<T>的作用惜纸,并通過TypeAdapters.JSON_ELEMENT了解了它的用法,很多時候绝骚,我們可能需要定制我們自己的TypeAdapter<T>耐版,以便適應(yīng)需求,那么接著走起压汪。
    final String jsonStr = "{'data':'this is data', 'result':{'name':'horseLai', 'age':24}}";
    //輸出:Data{data='this is data', result=User{name='horseLai', age=24}} 
    Gson gson = new GsonBuilder()
     .registerTypeAdapter(Data.class, new MyTypeAdapter())
     .enableComplexMapKeySerialization()
     .serializeNulls()
     .setDateFormat(DateFormat.LONG)
     .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
     .setPrettyPrinting()
     .setVersion(1.0)
     .create();
     
    TypeAdapter<Data> adapter = gson.getAdapter(Data.class);
    try {
        StringReader stringReader = new StringReader(jsonStr);
        Data read = adapter.read(new JsonReader(stringReader));
        System.out.println(read);
    } catch (IOException e) {
        e.printStackTrace();
    }
  • 注意粪牲,自定義TypeAdapter時,對于要解析的這個json數(shù)據(jù)止剖,gson是按照字段名->字段值逐條讀的(字段名稱(nextName)也會讀到腺阳,并且如果不先讀名稱而直接讀值(next<類型>)的話落君,會出錯),寫操作的話寫值就好了亭引,讀/寫有個共同的特點绎速,那就是一定要正確標識對象和數(shù)組的起始位置和終止位置,不然肯定會出錯痛侍,標識起始/終止位置的重要方法如下朝氓,JsonWriterJsonReader都有:
    • beginObject()用于標識馬上要寫入/讀取的是一個Json對象魔市;
    • endObject()用于標識這個Json對象已經(jīng)寫/讀完了主届;
    • beginArray()用于標識馬上要寫入/讀取的是一個Json數(shù)組;
    • endArray()用于標識這個Json數(shù)組已經(jīng)寫/讀完了待德;
// 對應(yīng)于我們的`Json`數(shù)據(jù)定制
static class MyTypeAdapter extends TypeAdapter<Data> {
    @Override
    public void write(JsonWriter out, Data value) throws IOException
    {
        if (value == null) {
            out.nullValue();
            return;
        } 
         out.setLenient(true);
            out.beginObject();
            out.name("data");
            out.value(value.data);

            out.name("result");
            out.beginObject();
            out.name("name");
            out.value(value.getResult().name);
            out.name("age");
            out.value(value.getResult().age);
            out.endObject();

            out.endObject();
    } 
    @Override
    public Data read(JsonReader in) throws IOException
    {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        Data data = new Data();
        User user = new User();
        in.setLenient(true);
        in.beginObject();
        while (in.hasNext()) {
            switch (in.nextName()) { // 先讀取字段名稱君丁,然后逐個去匹配
                case "data":
                    data.setData(in.nextString());
                    break;
                case "result":
                    in.beginObject();
                    break;
                case "name":
                    user.setName(in.nextString());
                    break;
                case "age":
                    user.setAge(in.nextInt());
                    break;
            }
        }
        data.setResult(user);
        in.endObject();
        in.endObject();
        return data;
    }
} 
 
  • 上面代碼中有個叫setLenient的方法,設(shè)置這個方法有啥作用呢将宪?可以追溯到下面源碼绘闷,其中lenient就是setLenient設(shè)置的值,可以看出它用于決定是否對value進行合法性檢查较坛,表示是否容忍印蔗,false表示0容忍,必須檢查丑勤,true表示容忍华嘹,你隨意。
 public JsonWriter value(Number value) throws IOException {
    // . . . 
    String string = value.toString();
    if (!lenient && (string.equals("-Infinity") 
            || string.equals("Infinity") || string.equals("NaN"))) {
      throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
    }
    beforeValue();
    out.append(string);
    return this;
  }
  • 至此法竞,可能我們會有所疑問耙厚,即便我們沒有注冊我們定制的TypeAdapter也可以通過toJsonfromJson解析數(shù)據(jù)岔霸,那它是通過誰解析的呢薛躬?我們繼續(xù)追蹤。

  • 其中getAdapter的偽代碼如下(源碼請自行查看)呆细,首先會從TokenCacheThreadLocal中查找型宝,看有沒有使用過的緩存,如果有絮爷,那么返回對應(yīng)的值趴酣,如果都沒有,那么會遍歷Gson中的工廠集合略水,逐一使用工廠去創(chuàng)建對應(yīng)于typeTypeAdapter价卤,顯然不匹配的話會返回null, 如果最終實在沒有找到合適的,那么只能拋異常了渊涝。

public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) { 
   if (TypeToken緩存中是否存在){
        return (TypeAdapter<T>) cached;
    } 
   if (ThreadLocal中是否保存有){
        return   (FutureTypeAdapter<T>) cached;
    }
    
    for (TypeAdapterFactory factory : factories) {
        TypeAdapter<T> candidate = factory.create(this, type);
        if (candidate != null) {
          call.setDelegate(candidate);   // 緩存至ThreadLocal
          typeTokenCache.put(type, candidate);  // 緩存至TokenCache
          return candidate;
        }
      } 
    // 實在沒找到可以匹配的 
      throw new IllegalArgumentException("GSON cannot handle " + type);
    // . . . 
}
  • 那么到誰是幕后操縱者呢慎璧?通過調(diào)試追蹤床嫌,最終定位到ReflectiveTypeAdapterFactory,從名稱上看就知道它用到了反射胸私。它有個嵌套類Adapter<T>, 其read方法源碼如下,可見實際上它的邏輯就是利用反射構(gòu)造一個對應(yīng)于T的對象厌处,然后從讀取Json字段,接著去T對象中找到相應(yīng)的字段岁疼,并通過反射賦值過去阔涉。至于write操作,原理是一致的捷绒,因此感興趣的童鞋可以查閱源碼瑰排。
 @Override public T read(JsonReader in) throws IOException {
     // . . . 
      T instance = constructor.construct();
        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);
          }       
        // . . .
        in.endObject();
      return instance;
    }

JsonParser

  • JsonParser用于將json數(shù)據(jù)轉(zhuǎn)換成JsonElement對象,它的使用方法非常簡單暖侨,如下示例:
       final String jsonStr = "{'data':'this is data', 'result':{'name':'horseLai', 'age':24}}";
        JsonParser jsonParser = new JsonParser();
        JsonObject jsonObject = jsonParser.parse(jsonStr).getAsJsonObject();
 
        String data = jsonObject.get("data").getAsString();
        JsonObject result = jsonObject.get("result").getAsJsonObject();

        Data data1 = new Data();
        data1.setData(data);
        User user = new User();
        user.setAge(result.get("age").getAsInt());
        user.setName(result.get("name").getAsString());
        data1.setResult(user);
        //輸出:Data{data='this is data', result=User{name='horseLai', age=24}}
        System.out.println(data1);
  • 顯然椭住,直接使用JsonParser需要耗費很多體力,因為我們需要手動提取json數(shù)據(jù)中的每一個字段屬性數(shù)據(jù)字逗,對于復(fù)雜度高的json數(shù)據(jù)解析而言京郑,可以說相當(dāng)?shù)姆爆嵙恕?/li>
  • 那么JsonParser背后做了什么呢?
    • 我們已經(jīng)知道了JsonParser#parse會將json數(shù)據(jù)解析成JsonElement對象,那么結(jié)合之前對Gson#fromJson方法的分析葫掉,我們可以肯定的是些举,它也會最終通過TypeAdaper#read方法來從Json數(shù)據(jù)流中讀取字段和值。
    • 我們查看一下JsonParser#parse源碼俭厚,可以很容易的定位到以下源碼户魏,得知他最終是通過TypeAdapters.JSON_ELEMENT來解析成JsonElement,至此套腹,處理流程就與前面分析的一致了绪抛。
 public JsonElement parse(JsonReader json) throws JsonIOException, JsonSyntaxException {
    // . . .
      return Streams.parse(json); 
  }

  // Streams#parse
 public static JsonElement parse(JsonReader reader) throws JsonParseException {
    // . . . 
      reader.peek(); 
      return TypeAdapters.JSON_ELEMENT.read(reader);  
  }

小結(jié)

  • 至此,我們已經(jīng)分析了Gson直接將json數(shù)據(jù)解析成java實體類电禀,以及將java實體類轉(zhuǎn)換成json的處理流程幢码,以下是Gson#toJson處理過程的總結(jié),對于Gson#fromJson 而言也是類似的尖飞。
Gson#toJson 
-> 建立JsonWriter.AppendableWriter 數(shù)據(jù)流
-> 創(chuàng)建對應(yīng)于我們所需數(shù)據(jù)類型的TypeToken
-> 根據(jù)TypeToken 查找TypeToken緩存中是否已經(jīng)存在已經(jīng)對應(yīng)的使用過的TypeAdapter
-> 根據(jù)TypeToken 查找ThreadLocal是否存在對應(yīng)的使用過的TypeAdapter
-> 如果沒有症副,則遍歷所有TypeAdapterFactory并創(chuàng)建對應(yīng)于我們所需類型的TypeAdapter,
   如果還沒匹配政基,那么也會ReflectiveTypeAdapterFactory贞铣,只要我們指定的類型是Java實體類的話,畢竟是通過反射操作沮明。
   Gson中實現(xiàn)的TypeAdapter幾乎已經(jīng)覆蓋到了我們常用的所有類型辕坝,具體可查閱TypeAdapters類。
-> 在TypeAdapter.write中提取Java實體類字段值寫入到流中荐健,轉(zhuǎn)換成json數(shù)據(jù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末酱畅,一起剝皮案震驚了整個濱河市琳袄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纺酸,老刑警劉巖窖逗,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異餐蔬,居然都是意外死亡碎紊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門樊诺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仗考,“玉大人,你說我怎么就攤上這事啄骇〕睁” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵缸夹,是天一觀的道長。 經(jīng)常有香客問我螺句,道長虽惭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任蛇尚,我火速辦了婚禮芽唇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘取劫。我一直安慰自己匆笤,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布谱邪。 她就那樣靜靜地躺著炮捧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惦银。 梳的紋絲不亂的頭發(fā)上咆课,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音扯俱,去河邊找鬼书蚪。 笑死,一個胖子當(dāng)著我的面吹牛迅栅,可吹牛的內(nèi)容都是我干的殊校。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼读存,長吁一口氣:“原來是場噩夢啊……” “哼为流!你這毒婦竟也來了窜醉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤艺谆,失蹤者是張志新(化名)和其女友劉穎榨惰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體静汤,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡琅催,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了虫给。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片藤抡。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖抹估,靈堂內(nèi)的尸體忽然破棺而出缠黍,到底是詐尸還是另有隱情,我是刑警寧澤药蜻,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布瓷式,位于F島的核電站,受9級特大地震影響语泽,放射性物質(zhì)發(fā)生泄漏贸典。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一踱卵、第九天 我趴在偏房一處隱蔽的房頂上張望廊驼。 院中可真熱鬧,春花似錦惋砂、人聲如沸妒挎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酝掩。三九已至,卻和暖如春罗标,著一層夾襖步出監(jiān)牢的瞬間庸队,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工闯割, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留彻消,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓宙拉,卻偏偏與公主長得像宾尚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容