美文网首页
从使用到源码—Gson(上)

从使用到源码—Gson(上)

作者: horseLai | 来源:发表于2018-08-13 00:01 被阅读0次

    引言

    • 使用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数据
    

    相关文章

      网友评论

          本文标题:从使用到源码—Gson(上)

          本文链接:https://www.haomeiwen.com/subject/rcnebftx.html