从使用到源码—Gson(上)

317 阅读9分钟

引言

  • 使用Gson已经有好长时间了,但是一直停留在使用的层面上,因此在对它的好奇心尚未消失之前,我得跟它做个了断 。
  • 关于Gson,将会有几篇长篇解读文,用于记录自己浅显的见解。

使用默认配置的Gson

  • 对于Json数据的解析,我们总是希望能够只是通过input -> output过程就能得到想要的结果,而不用手动去逐个解析相关字段,因为太费事了。出于这种需求,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发生了什么?

    • 我们可以思考一下,如果我们需要从json字符串中分离出字段名及其对应的值,然后将这些值填充赋值到Java实体类中对应的字段中,我们要做哪些工作,如何进行?

      • 我的思路
        • 分解json字符串,直觉是使用正则,但是仔细思考一番会发现,对于简单的json数据来说,Ok,但是对象层级深一点的话就复杂了,有没有更好的方式呢?使用栈和Map配合的方式,遇到{[,标记为对象或数组的开始,然后往栈中压入数据,遇到:则表示读取到了一个字段的名称,接着去除''和一些空白符号后将字段名作为Mapkey,清空栈,接着压栈,此时要解析值,那么可以将' ', '}] '}等作为值的起点或终点,标记,取值,最终将字段名和值存到Map中,那么这种方法对于复杂多层级多类型的json数据而言也是OK的,只要标记好是对象还是数组还是普通数据类型,然后在Map中做好对应于Json数据的层级即可。如果数据是HTML的话,会比较不好判断,因为其中包含的符号可能会比较多,干扰判断。
        • 分离了json数据中的字段名称和值,接着填充赋值实体类就容易了,反射通过data.getClass().getField("name").set(xxx)的方式一条龙服务。
    • 思考过后,带着疑问与好奇,通过追踪,在Gson#fromJson方法中有如下代码,可以看出,当使用Gson#fromJsonJson数据转换到Java实体类时,会最终调用TypeAdapter#read方法进行数据填充操作。

    public <T> T fromJson(String json, Class<T> classOfT) throws JsonIOException, JsonSyntaxException  {
        // 省略无关代码、调用层次若干..
    
        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方法中有如下代码,可以看出,当我们使用Gson#fromJsonJson数据转换到我们的Java实体类时,会最终调用TypeAdapter#write方法进行数据填充操作。
    public void toJson(Object src) throws JsonIOException {
        // 省略无关代码、调用层次若干..
        // 最终会调用
      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>() 
    { // 省略无关代码若干..}
    
  • 那么TypeAdapter<T>有什么用,怎么用?

    • TypeAdapter<T>,类型适配器,包含write``read两个核心抽象方法,用于往数据流中写/读数据。
    public abstract class TypeAdapter<T> {
        // ...
         public abstract void write(JsonWriter out, T value) throws IOException;
         public abstract T read(JsonReader in) throws IOException;
    }
    
    • TypeAdapters中包含有它的若干个基本实现类,比如在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的栈中拿到对应的字段类型,接着去匹配、读取
          case STRING:  
            return new JsonPrimitive(in.nextString());
           //. . .
          case BEGIN_ARRAY:
          // 数组和对象是比较特殊的,因为他们有自己的字段、对象或数组,
          // 并代表着层级的深度,因此读写时都需要注意标识好
            JsonArray array = new JsonArray();
            in.beginArray(); // 标识数组起点,告诉它这是个数组
            while (in.hasNext()) { // 接着通过递归读取、匹配字段类型,并放到数组中
              array.add(read(in));
            }
            in.endArray(); // 告诉gson,这个数组已经读完了
            return array;
          // . . .  
        } 
        // 上面分析了从流中读取并构建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()) { // 写数组,跟读一个道理啦
            out.beginArray(); // 标识起点
            for (JsonElement e : value.getAsJsonArray()) {
              write(out, e); 
            }
            out.endArray();  // 标识终点
          }// . . .
        }
      };
    

定制使用TypeAdapter<T>

  • 上面分析了TypeAdapter<T>的作用,并通过TypeAdapters.JSON_ELEMENT了解了它的用法,很多时候,我们可能需要定制我们自己的TypeAdapter<T>,以便适应需求,那么接着走起。
    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数据,gson是按照字段名->字段值逐条读的(字段名称(nextName)也会读到,并且如果不先读名称而直接读值(next<类型>)的话,会出错),写操作的话写值就好了,读/写有个共同的特点,那就是一定要正确标识对象和数组的起始位置和终止位置,不然肯定会出错,标识起始/终止位置的重要方法如下,JsonWriterJsonReader都有:
    • beginObject()用于标识马上要写入/读取的是一个Json对象;
    • endObject()用于标识这个Json对象已经写/读完了;
    • beginArray()用于标识马上要写入/读取的是一个Json数组;
    • endArray()用于标识这个Json数组已经写/读完了;
// 对应于我们的`Json`数据定制
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的方法,设置这个方法有啥作用呢?可以追溯到下面源码,其中lenient就是setLenient设置的值,可以看出它用于决定是否对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解析数据,那它是通过谁解析的呢?**我们继续追踪。

  • 其中getAdapter的伪代码如下(源码请自行查看),首先会从TokenCacheThreadLocal中查找,看有没有使用过的缓存,如果有,那么返回对应的值,如果都没有,那么会遍历Gson中的工厂集合,逐一使用工厂去创建对应于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);
    // . . . 
}
  • **那么到谁是幕后操纵者呢?**通过调试追踪,最终定位到ReflectiveTypeAdapterFactory,从名称上看就知道它用到了反射。它有个嵌套类Adapter<T>, 其read方法源码如下,可见实际上它的逻辑就是利用反射构造一个对应于T的对象,然后从读取Json字段,接着去T对象中找到相应的字段,并通过反射赋值过去。至于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数据转换成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数据中的每一个字段属性数据,对于复杂度高的json数据解析而言,可以说相当的繁琐了。
  • 那么JsonParser背后做了什么呢?
    • 我们已经知道了JsonParser#parse会将json数据解析成JsonElement对象,那么结合之前对Gson#fromJson方法的分析,我们可以肯定的是,它也会最终通过TypeAdaper#read方法来从Json数据流中读取字段和值。
    • 我们查看一下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);  
  }

小结

  • 至此,我们已经分析了Gson直接将json数据解析成java实体类,以及将java实体类转换成json的处理流程,以下是Gson#toJson处理过程的总结,对于Gson#fromJson 而言也是类似的。
Gson#toJson 
-> 建立JsonWriter.AppendableWriter 数据流
-> 创建对应于我们所需数据类型的TypeToken
-> 根据TypeToken 查找TypeToken缓存中是否已经存在已经对应的使用过的TypeAdapter
-> 根据TypeToken 查找ThreadLocal是否存在对应的使用过的TypeAdapter
-> 如果没有,则遍历所有TypeAdapterFactory并创建对应于我们所需类型的TypeAdapter,
   如果还没匹配,那么也会ReflectiveTypeAdapterFactory,只要我们指定的类型是Java实体类的话,毕竟是通过反射操作。
   Gson中实现的TypeAdapter几乎已经覆盖到了我们常用的所有类型,具体可查阅TypeAdapters类。
-> 在TypeAdapter.write中提取Java实体类字段值写入到流中,转换成json数据