【Gson源码分析】- 彻底搞懂Gson解析流程

简介

Gson是google提供的一款Json解析框架,基本很多项目都会使用到。Gson自带的容错机制能够使解析过程更加友好,但是并不能帮助我们解决所以的容错问题,这时候可以通过向Gson注册自己的解析适配器来接管Gson的解析过程。下面将通过分析源码的方式,了解Gson内部实现原理和解析流程。带大家彻底搞懂Gson

重点

  • 扩展Gson容错机制
  • Gson注解使用
  • Gson解析流程

场景

  • 在解析Json数据的时候,由于后台返回的json数据格式的不规范,偶现解析数据崩溃,当然Gson自身就有一定的容错机制,但是,有些时候并不能到达项目的需要。比如:Gson对int数据的解析,当后台返回"123"和""时,前者由于Gson自身的容错处理能够正常解析,但是后者却会导致应用崩溃,其实我们更希望将""解析成“0”而不是应用崩溃。
  • 有时候,由于后台返回的json数据中某个字段的名字不一样,导致我们不得不在数据实体里面新加字段或者新创建一个实体类,这样不但会增加多余的代码,同时让代码逻辑变得更加混乱,后期难以维护。
  • 配置某些字段在序列化和反序列化过程中的行为。

fromJson(反序列化) and toJson(序列化)

这两个方法最重要的地方都是,获取一个TypeAdapter对象,调用read和write完成反序列化和序列化过程。完整代码查看 getAdapter(TypeToken<T> type)方法。

TypeAdapter<?> cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type);
  if (cached != null) {
    return (TypeAdapter<T>) cached;
  }

从缓存获取TypeAdapter对象,存在者直接返回

FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type);
  if (ongoingCall != null) {
    return ongoingCall;
  }

通过ThreadLocal缓存TypeAdapter对象,不同的线程使用缓存来解析的时候互不影响。

for (TypeAdapterFactory factory : factories) {
   TypeAdapter<T> candidate = factory.create(this, type);
    if (candidate != null) {
        call.setDelegate(candidate);
        typeTokenCache.put(type, candidate);
        return candidate;
    }
}

如果不存在缓存,那么从factories列表里查找,factories是在创建Gson对象时初始化,添加了很多用于创建TypeAdapter对象的TypeAdapterFactory。

  • fromJson(反序列化)
    实例:
private fun test(){
    val data = "{" +
         "\"errcode\": \"\"," +
         "\"errmsg\": \"success\"" +
        "}"
    val item = new Gson().fromJson(data, GsonItem::class.java)
}
  public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException {
    ...
      reader.peek();
      ...
      TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
      TypeAdapter<T> typeAdapter = getAdapter(typeToken);
      T object = typeAdapter.read(reader);
      return object;
      ...
  }
  1. reader.peek()
    解析字符串第一个字符在json格式里的类型。
  2. getAdapter(typeToken)
    通过getAdapter(TypeToken<T> type)方法获取TypeAdapter对象,分两种情况:
    1. 类使用了@JsonAdapter
      看一下Gson初始化“factories”数组时的顺序,添加JsonAdapterAnnotationTypeAdapterFactory对象在ReflectiveTypeAdapterFactory对象之前??匆幌耤reate方法:
      @SuppressWarnings("unchecked")
      @Override
      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType)     {
         Class<? super T> rawType = targetType.getRawType();
         JsonAdapter annotation = rawType.getAnnotation(JsonAdapter.class);
         if (annotation == null) {
             return null;
         }
         return (TypeAdapter<T>) getTypeAdapter(constructorConstructor, gson, targetType, annotation);
      }
      
      如果对实体类使用了@JsonAdapter且指定的适配器存在那么就会返回@JsonAdapter里指定的适配器而不返回ReflectiveTypeAdapterFactory创建的,这样我们就可以自己接管后面的解析过程了,具体用法参考后面给出的工程源码。
    2. 没有使用@JsonAdapter注解:
      这里要注意,对于基础知识不太牢固的人,可能会认为这里返回的是ObjectTypeAdapter实例,认为所以类都都继承于Object,所以GsonItem.class == Object.class为true,其实是不等的,这里返回的应该是ReflectiveTypeAdapterFactory实例,调用ReflectiveTypeAdapterFactory里的create返回内部Adapter对象。
      @Override public <T> TypeAdapter<T> create(Gson gson, final       TypeToken<T> type) {
        ...
        // constructorConstructor = new ConstructorConstructor(instanceCreators);
        ObjectConstructor<T> constructor = constructorConstructor.get(type);
        return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
      }
    
    • getBoundFields(gson, type, raw)
      将实体类中需要解析的字段添加一个集合里,在反序列化时进行赋值。

      1. 得到实体类所以的字段
        Field[] fields = raw.getDeclaredFields();
        
      2. 字段是否参与反序列化或者序列化过程
        boolean serialize = excludeField(field, true);
        boolean deserialize = excludeField(field, false);
        
        static boolean excludeField(Field f, boolean serialize, Excluder excluder) {
          return !excluder.excludeClass(f.getType(), serialize) &&     !excluder.excludeField(f, serialize);
        }
        

      3.excludeClassChecks(clazz)检查class类型是否符合序列化或者反序列化要求,这里可以自己点击去看一下。里面用到的Since和Until注解,作用于类,和作用于字段意思一样,将在下面讲解。

      1. excludeClassInStrategy(clazz, serialize)通过加入自己的策略来控制字段是否要参与解析,在初始化的时候可以加入自己的策略。如果某个字段不符合Gson解析要求,但是你觉得可以正常解析,那么就可以在自己的策略返回true。
       private boolean excludeClassInStrategy(Class<?> clazz, boolean serialize) {
       List<ExclusionStrategy> list = serialize ? serializationStrategies : deserializationStrategies;
       for (ExclusionStrategy exclusionStrategy : list) {
          if (exclusionStrategy.shouldSkipClass(clazz)) {
              return true;
          }
        }
          return false;
        }
      
      1. excluder.excludeField(f, serialize)过滤字段

        \color{blue}{注解:}@Since / @Until

        在配置了new GsonBuilder().setVersion(double v)时,@Since(double v)、@Until(double v)才起作用。这个查看源码可以得知。

        查看isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))方法,可以得出这两个注解的用法如下:
        比如:

        /**该属性自2.2+版本开始弃用*/
        @Until(2.2)
        private String sex;
        
        /**该属性自1.3+版本 开始启用*/
        @Since(1.3)
        private String name;
        
        /**该属性自1.4+版本开始弃用*/
        @Until(1.4)
        private String number;
        
      \color{blue}{注解:}@Expose

      是否将字段暴露出去,参与序列化和反序列化。需要 GsonBuilder 配合 .excludeFieldsWithoutExposeAnnotation() 方法使用,否则不起作用。

      if (requireExpose) {
          Expose annotation = field.getAnnotation(Expose.class);
          if (annotation == null || (serialize ? !annotation.serialize() : !annotation.deserialize())) {
              return true;
           }
        }
      

      返回true表示不解析该字段。这个注解使用时,请注意看这里的判断逻辑,不然很可能发现根本解析不出数据来。

      过滤策略

      List<ExclusionStrategy> list = serialize ? serializationStrategies : deserializationStrategies;
      if (!list.isEmpty()) {
      FieldAttributes fieldAttributes = new FieldAttributes(field);
      for (ExclusionStrategy exclusionStrategy : list) {
           if (exclusionStrategy.shouldSkipField(fieldAttributes)) {
              return true;
            }
          }
       }
      
    1. 获取字段类型

       Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
      
    2. 获取字段名字

      List<String> fieldNames = getFieldNames(field)
      
      \color{blue}{注解:}@SerializedName

      @SerializedName 可以用来配置 JSON 字段的名字,比如:在同一个 Test 对象中的用户电话,现在不同的接口返回不同的字段,比如: phone、user_phone、userphone,这种差异也可以用 @SerializedName .来解决。

      class Test{
          @SerializedName("user_phone")
          var userPhone :String? = null
          var sex = 0
      }
      

      在 @SerializedName 中,还有一个 alternate 字段,可以对同一个字段配置多个解析名称。

      class Test{
          @SerializedName(value = "user_phone",alternate = arrayOf("phone","userphone"))
          var userPhone :String? = null
          var sex = 0
      }
      
      \color{red}{注意:}

      一旦使用@SerializedName后,字段本身的名字不在起作用,所以需要指定@SerializedName中value的值。

    3. createBoundField(...)

      final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
      

      是否是基本数据类型

      \color{blue}{注解:}@JsonAdapter

      Gson的使用者可以根据实际的需要对某个具体类型的序列化和反序列化的转换进行控制,可放置在属性上。

       JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
       TypeAdapter<?> mapped = null;
       if (annotation != null) {
           mapped = jsonAdapterFactory.getTypeAdapter(
           constructorConstructor, context, fieldType, annotation);
       }
      

      如果实体类某属性使用了@JsonAdapter,那么该属性的序列化和反序列化将由指定的适配器接管。如果没有这会从Gson初始化中查找对于的解析适配器。

  3. typeAdapter.read(reader)
    1. 创建实体类对象
      T instance = constructor.construct();
      
      具体怎样创建的,请查看源码,也比较简单,这个过程也是可以通过扩展相关类来接管的。
    2. Json流开始的类型,并做上相应标记。
       in.beginObject();
      
    3. 读值
      String name = in.nextName();
        BoundField field = boundFields.get(name);
        if (field == null || !field.deserialized) {
          in.skipValue();
        } else {
          field.read(in, instance);
        }
      
      获取Json数据中的name,如果在 boundFields(需要反序列化的字段)没有者跳过,如果有,者读取对应值并赋值给实体类对应字段。
      1. field.read(in, instance)
        @Override void read(JsonReader reader, Object value)
        throws IOException, IllegalAccessException {
           Object fieldValue = typeAdapter.read(reader);
           if (fieldValue != null || !isPrimitive) {
               field.set(value, fieldValue);
           }
        }
        
        读取值并赋值给实体类,至于怎么读取的,可以看一下Gson初始化里面,已经添加的解析适配器。
  • toJson(序列化)
    基本流程和大部分实现都和fromJson(反序列化)相同,请自行查看源码。
  • 补充
    1. gson = new GsonBuilder().setDateFormat("yyyy-MM").create()可以设置日期类型在序列化和反序列化过程输出的格式。Gson提供了DefaultDateTypeAdapter和DateTypeAdapter来进行转换。
    2. GsonBuilder()
      .registerTypeAdapter(Int::class.java, IntDeserializerAdapter())
      . registerTypeHierarchyAdapter(String::class.java, NumberTypeAdapter())
      .create()
      .fromJson<GsonItem>(jsonStr,GsonItem::class.java)
      注册自己的解析适配器,代替Gson自带的。
      override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Int 
      = try {
      json!!.asInt
      } catch (e: NumberFormatException) {
          0
      }
      
      Gson能够把类似"123"解析成Int,但是如果是""者会抛异常导致崩溃,所以我们可以接管反序列化过程,当出现异常时候返回0。
  1. registerTypeAdapter() 和registerTypeHierarchyAdapter()区别
    前者要求我们传递一个明确的类型,也就是说它不支持继承,而 后者 则可以支持继承。
  • 对补充中的第二点补充
    使用GsonBuilder()
    .registerTypeAdapter()或者GsonBuilder(...)
    .registerTypeHierarchyAdapter(...)注册的适配器是继承于TypeAdapter而不是JsonDeserializer,你发现怎么try...catch应用都会崩溃。这里看一下有什么不同,找到TreeTypeAdapter类的read方法,至于为什么是这个类,请按照这篇文章逻辑梳理一遍。
 @Override public T read(JsonReader in) throws IOException {
   if (deserializer == null) {
     return delegate().read(in);
   }
   JsonElement value = Streams.parse(in);
   if (value.isJsonNull()) {
     return null;
   }
   return deserializer.deserialize(value, typeToken.getType(), context);
 }

如果deserializer不等于null,这反序列化过程由继承于JsonDeserializer的类接管。那么看一下为什么继承TypeAdapter会有问题,定位到自定义的TypeAdapter的read方法

override fun read(i: JsonReader): Int? {
     if (i.peek() == JsonToken.NULL) {
         i.nextNull()
         return 0
     }
    try {
         return i.nextInt()
    } catch (e: Exception) {
            return 0
   }
}

看一下i.nextInt()

  public int nextInt() throws IOException {
    ...
    try {
        result = Integer.parseInt(peekedString);
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return result;
      } catch (NumberFormatException ignored) {
      }
    } else {
      throw new IllegalStateException("Expected an int but was " + peek() + locationString());
    }
        ...
    peeked = PEEKED_BUFFERED;
    ...

如果 Integer.parseInt(peekedString)出现异常,那么peeked = PEEKED_BUFFERED;由于try...catch,所以不会崩溃。
接下来获取下一个json数据中的name,定位到ReflectiveTypeAdapterFactory中的read方法里的String name = in.nextName()方法,当peeked = PEEKED_BUFFERED抛出异常,导致程序崩溃,解决办法就是自己写解析流程而不是简单的try...catch。

更多用法请查看下面工程代码
\color{blue}{完整代码}SimpleGson

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容

  • 1.概述2.Gson的目标3.Gson的性能和扩展性4.Gson的使用者5.如何使用Gson 通过Maven来使用...
    人失格阅读 14,226评论 2 18
  • 概况 Gson是一个Java库,它可以用来把Java对象转换为JSON表达式,也可以反过来把JSON字符串转换成与...
    木豚阅读 6,783评论 0 2
  • 泛型: https://juejin.im/post/5b614848e51d45355d51f792#headi...
    RexHuang阅读 6,843评论 0 0
  • 牽掛是一種說不出的痛,痛中有樂。 更加是一種改不了的癡,癡中有甜。 **人生如夢** **夢中,...
    把幸福搞丢了阅读 281评论 0 0
  • 昨天早晨,朋友说要去做直肠镜,问要不要一起去。我没加思索就拒绝了,并不是说我对自己的肠子多有信心,事实上,我连做直...
    银子姐阅读 1,231评论 5 6