Gson异常数据解析之通过源码寻找解决方法(一)

在前后端的数据交互中,JSON是最常用的数据交换格式,但是在交互的过程中总会有一些奇葩的数据出现,对于后端来说影响可能还不是很大,但是对于Android端来说,那就是暴击呀(程序已停止运行),这 除了怼死写这些数据的同事 就需要我们想办法去解决。在Android开发中我们使用的最多的JSON解析库就是Gson,当然在后端中可能也会有人使用,所以本篇学习的是使用Gson库(Gson2.8.5)时如何处理奇葩的数据。

本文需要对Gson有一定的了解才能看得懂,如果对Gson还不是很了解的同学推荐去看看怪盗kidou大佬写的Gson系列:你真的会用Gson吗?Gson使用指南。本文只是提供Gson解析异常数据的思路,实验所使用的Json数据只是举例说明,在实际应用中还是得根据具体的业务需求来对数据进行处理。

本篇的主要内容

  • String 类型数据的处理
  • Number(Integer,Float,Double,Long…) 类型数据的处理
  • Collection(List,Set,Map…) 类型数据的处理

由于实验需要,我们先准备以下几个实体类:

Result.java

public class Result {
        private Integer code;
        private String msg;
        private T data;

        //getters,setters,toString
}

Province.java

public class Province {
        private Integer id;
        private String name;
        private List cities;

        //getters,setters,toString
}

City.java

public class City {
        private Integer id;
        private String name;

        //getters,setters,toString
}

一、最常见之 String null 转为 空字符串

现在我们通过接口请求到了一段JSON数据,需要显示其中的name

 {
        "code":200,
        "msg":"success",
        "data":{
            "id":1,
            "name":null
        }
}

如果直接TextView.setText(City.getName()),那程序肯定直接挂了,所以得先进行非空判断,但是如果每个取值的地方都写个非空判断,那复制粘贴都得写到手残呀。我们作为具有高智商的程序员,肯定得想出解决偷懒的办法在某个地方统一处理掉null的数据。因为我们是使用Gson进行解析,所以我们先去看看源码中是怎么处理String类型的数据的。

在源码中我们找到了一个可疑的类com.google.gson.internal.bind.TypeAdapters,发现里面有很多TypeAdapter,在里面我们找到了关于String类型的处理

可以看到在反序列化时,当peek == JsonToken.NULL会返回null,所以我们可以在这上面通过自定义对序列化以及反序列化做处理,看看效果如何。

1、创建一个StringAdapter.java

    public class StringAdapter extends TypeAdapter {

        @Override
        public void write(JsonWriter out, String value) throws IOException {
            if (value == null) {
                out.value("");
                return;
            }
            out.value(value);
        }

        public String read(JsonReader reader) throws IOException {
            if (reader.peek() == JsonToken.NULL) {
                reader.nextNull();
                return "";
            }
            return reader.nextString();
        }
    }

2.使用方法

    Gson gson = new GsonBuilder().serializeNulls()
                    .registerTypeAdapter(String.class, new StringAdapter())
                    .create();
    //序列化
    Result result = new Result<>();
    String resultJson = gson.toJson(new Result<>());
    Log.e("序列化结果:", resultJson);
    //反序列化
    String jsonStr = "{\"code\":200,\"msg\":\"success\",\"data\":{\"id\":1,\"name\":null}}";
    Result result = gson.fromJson(jsonStr, new TypeToken>() {}.getType());
    Log.e("反序列化结果:", result.toString());

3.测试结果

    序列化结果:
    {"code":null,"data":null,"msg":""}

    反序列化结果:
    Result{code=200, msg='success', data=City{id=1, name=''}}

可以看到name由null转为了空字符串,实验成功!

二、特殊需求之Number类型的处理

现在我们通过接口请求到了一段JSON数据,需要取code出来判断

    {
        "code":"",
        "msg":"success",
        "data":{
            "id":1,
            "name":"Beijing"
        }
    }

如果解析肯定会抛出com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: empty String。通过上面的学习,同学们看到这里肯定已经想到了通过参考源码中Integer类型的处理来自定义解析。

1、创建一个IntegerAdapter.java

public class IntegerAdapter extends TypeAdapter {
    @Override
    public void write(JsonWriter out, Number value) throws IOException {
        if (value == null) {
            out.value(0);
            return;
        }
        out.value(value);
    }

    public Number read(JsonReader reader) throws IOException {
        if (reader.peek() == JsonToken.NULL) {
            reader.skipValue();
            return 0;
        } else if (reader.peek() == JsonToken.STRING) {
            try {
                return Integer.valueOf(reader.nextString());
            } catch (NumberFormatException e) {
                e.printStackTrace();
                return 0;
            } catch (IOException e) {
                e.printStackTrace();
                return 0;
            }
        }

        try {
            return reader.nextInt();
        } catch (NumberFormatException e) {
            throw new JsonSyntaxException(e);
        }
    }
}

2.使用方法

    Gson gson = new GsonBuilder().serializeNulls()
                    .registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, new IntegerAdapter()))
                    .create();
    //序列化
    Result result = new Result<>();
    String resultJson = gson.toJson(new Result<>());
    Log.e("序列化结果", resultJson);
    //反序列化
    String jsonStr = "{\"code\":\"\",\"msg\":\"success\",\"data\":{\"id\":1,\"name\":\"Beijing\"}}";
    Result result = gson.fromJson(jsonStr, new TypeToken>() {}.getType());
    Log.e("反序列化结果", result.toString());

3.测试结果

    序列化结果:
    {"code":0,"data":null,"msg":""}

    反序列化结果:
    Result{code=0, msg='success', data=City{id=1, name='Beijing'}}

可以看到code由空字符串转为了0,实验成功!

Float,Double,Long等Number类型可参考以上做法。

三、Collection 为 null的处理

现在我们通过接口请求到了一段JSON数据,需要获得省市列表

    {
        "code":200,
        "msg":"success",
        "data":[
            {
                "id":1,
                "name":"Beijing",
                "cities":null
            },
            {
                "id":2,
                "name":"Guangdong",
                "cities":[
                    {
                        "id":1,
                        "name":"Guangzhou"
                    },
                    {
                        "id":2,
                        "name":"Shenzhen"
                    }
                ]
            }
        ]
    }

我们在做省市联动选择列表时,一般对于直辖市的城市列表都是传一个空列表,但是很多时候后端传回来的是一个null,因为直辖市下查不到城市,这时候就需要做判断Province.getCities()!=null。此处只是列举省市列表场景,实际中可能还有其他的场景,需要对每个List的调用都进行非空判断。这时候我们就需要通过Gson直接把null List转为 empty List

有了上面的学习,我们很熟练的就去Gson源码里面查看,此处捕获一只野生的com.google.gson.internal.bind.CollectionTypeAdapterFactory

我们使用CV大法熟练的复制一份出来

    public final class CollectionTypeAdapterFactory implements TypeAdapterFactory {
      private final ConstructorConstructor constructorConstructor;

      public CollectionTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
        this.constructorConstructor = constructorConstructor;
      }

      @Override
      public  TypeAdapter create(Gson gson, TypeToken typeToken) {
        Type type = typeToken.getType();

        Class rawType = typeToken.getRawType();
        if (!Collection.class.isAssignableFrom(rawType)) {
          return null;
        }

        Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
        TypeAdapter elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
        ObjectConstructor constructor = constructorConstructor.get(typeToken);

        @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter
        TypeAdapter result = new Adapter(gson, elementType, elementTypeAdapter, constructor);
        return result;
      }

      private static final class Adapter extends TypeAdapter> {
        private final TypeAdapter elementTypeAdapter;
        private final ObjectConstructor> constructor;

        public Adapter(Gson context, Type elementType,
            TypeAdapter elementTypeAdapter,
            ObjectConstructor> constructor) {
          this.elementTypeAdapter =
              new TypeAdapterRuntimeTypeWrapper(context, elementTypeAdapter, elementType);
          this.constructor = constructor;
        }

        @Override public Collection read(JsonReader in) throws IOException {
          if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
          }

          Collection collection = constructor.construct();
          in.beginArray();
          while (in.hasNext()) {
            E instance = elementTypeAdapter.read(in);
            collection.add(instance);
          }
          in.endArray();
          return collection;
        }

        @Override public void write(JsonWriter out, Collection collection) throws IOException {
          if (collection == null) {
            out.nullValue();
            return;
          }

          out.beginArray();
          for (E element : collection) {
            elementTypeAdapter.write(out, element);
          }
          out.endArray();
        }
      }
    }

但是发现了一个错误


赶紧到源码中查看com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper,发现居然是使用默认访问修饰符(Java中不写访问修饰符时默认的访问修饰符是default,对于同一个包中的其他类相当于公开的(public),对于不是同一个包中的其他类相当于私有的(private))。

所以我们是不能直接使用了,只好再次施展CV大法(避免注释占用行数,下面代码已经把源码中的注释去掉)。

    public class TypeAdapterRuntimeTypeWrapper extends TypeAdapter {
        private final Gson context;
        private final TypeAdapter delegate;
        private final Type type;

        TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter delegate, Type type) {
            this.context = context;
            this.delegate = delegate;
            this.type = type;
        }

        @Override
        public T read(JsonReader in) throws IOException {
            return delegate.read(in);
        }

        @SuppressWarnings({"rawtypes", "unchecked"})
        @Override
        public void write(JsonWriter out, T value) throws IOException {
            TypeAdapter chosen = delegate;
            Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
            if (runtimeType != type) {
                TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType));
                if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {      
                    chosen = runtimeTypeAdapter;
                } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
                    chosen = delegate;
                } else {
                    chosen = runtimeTypeAdapter;
                }
            }
            chosen.write(out, value);
        }

        private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
            if (value != null
                    && (type == Object.class || type instanceof TypeVariable || type instanceof Class)) {
                type = value.getClass();
            }
            return type;
        }
    }

1、修改CollectionTypeAdapterFactoryAdapterreadwrite方法

由于代码太长,这里只给出修改部分的代码

    public Collection read(JsonReader in) throws IOException {
      if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          //源码中是 return null,改为 return empty constructor
          return constructor.construct();
      }
      //more than
    }

    public void write(JsonWriter out, Collection collection) throws IOException {
    if (collection == null) {
        //源码中是 out.nullValue(),改为 []
        out.beginArray();
        out.endArray();
        return;
    }

2.使用方法

        Gson gson = new GsonBuilder().serializeNulls()
                        .registerTypeAdapterFactory(new CollectionTypeAdapterFactory(new ConstructorConstructor(new HashMap>())))
                        .create();
        //序列化
        List provinces = new ArrayList<>();
        Province province = new Province();
        province.setId(1);
        province.setName("Beijing");
        provinces.add(province);

        Result> result = new Result<>();
        result.setData(provinces);

        String resultJson = gson.toJson(result);
        Log.e("序列化结果", resultJson);
        //反序列化
        String jsonStr = "{\"code\":200,\"msg\":\"success\",\"data\":[{\"id\":1,\"name\":\"Beijing\",\"cities\":null},{\"id\":2,\"name\":\"Guangdong\",\"cities\":[{\"id\":1,\"name\":\"Guangzhou\"},{\"id\":2,\"name\":\"Shenzhen\"}]}]}";
        Result result = gson.fromJson(jsonStr, new TypeToken>() {}.getType());
        Log.e("反序列化结果", result.toString());

对于register的时候有一个情况,在源码中是使用GsonBuilder中的Map> instanceCreators,即new CollectionTypeAdapterFactory(new ConstructorConstructor(instanceCreators)),我们来看一下这个instanceCreators是个啥玩意

可以看到它是当register了InstanceCreator类型的TypeAdapter时才起作用,否则直接使用new HashMap也是可以的。由于instanceCreatorsGsonBuilder中是private的,所以我们只能通过反射来获取

    GsonBuilder gsonBuilder = new GsonBuilder();
    try {
        Class builder = gsonBuilder.getClass();
        Field f = builder.getDeclaredField("instanceCreators");
        f.setAccessible(true);
        Map> val = (Map>) f.get(gsonBuilder);
        builder.registerTypeAdapterFactory(new CollectionTypeAdapterFactory(new ConstructorConstructor(val)));
    } catch (NoSuchFieldException | IllegalAccessException e) {
        e.printStackTrace();
        builder.registerTypeAdapterFactory(new CollectionTypeAdapterFactory(new ConstructorConstructor(new HashMap>())));
    }

3.测试结果

序列化结果:
{"code":null,"data":[{"cities":[],"id":1,"name":"Beijing"},{"cities":[],"id":2,"name":"Beijing"}],"msg":null}

 反序列化结果:
Result{code=200, msg='success', data=[Province{id=1, name='Beijing', cities=[]}, Province{id=2, name='Guangdong', cities=[City{id=1, name='Guangzhou'}, City{id=2, name='Shenzhen'}]}]}

可以看到cities由null转为了[],实验成功!

为了避免篇幅过长,所以将文章拆分成了两篇,想继续学习研究的同学可以看

  • Gson异常数据解析之通过源码寻找解决方法(二)

记录,分享,交流。

你可能感兴趣的:(Gson异常数据解析之通过源码寻找解决方法(一))