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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/choimroc/article/details/89300883

在前后端的数据交互中,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<T> {
    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<City> 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<String> {

    @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<City> result = new Result<>();
String resultJson = gson.toJson(new Result<>());
Log.e("序列化结果:", resultJson);
//反序列化
String jsonStr = "{\"code\":200,\"msg\":\"success\",\"data\":{\"id\":1,\"name\":null}}";
Result<City> result = gson.fromJson(jsonStr, new TypeToken<Result<City>>() {}.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<Number> {

    @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<City> result = new Result<>();
String resultJson = gson.toJson(new Result<>());
Log.e("序列化结果", resultJson);
//反序列化
String jsonStr = "{\"code\":\"\",\"msg\":\"success\",\"data\":{\"id\":1,\"name\":\"Beijing\"}}";
Result<City> result = gson.fromJson(jsonStr, new TypeToken<Result<City>>() {}.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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
    Type type = typeToken.getType();

    Class<? super T> 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<T> constructor = constructorConstructor.get(typeToken);

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

  private static final class Adapter<E> extends TypeAdapter<Collection<E>> {
    private final TypeAdapter<E> elementTypeAdapter;
    private final ObjectConstructor<? extends Collection<E>> constructor;

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

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

      Collection<E> 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<E> 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<T> extends TypeAdapter<T> {
    private final Gson context;
    private final TypeAdapter<T> delegate;
    private final Type type;

    TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> 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<E> 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<E> 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<Type, InstanceCreator<?>>())))
                .create();
//序列化
List<Province> provinces = new ArrayList<>();
Province province = new Province();
province.setId(1);
province.setName("Beijing");
provinces.add(province);

Result<List<Province>> 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<City> result = gson.fromJson(jsonStr, new TypeToken<Result<City>>() {}.getType());
Log.e("反序列化结果", result.toString());

对于register的时候有一个情况,在源码中是使用GsonBuilder中的Map<Type, InstanceCreator<?>> 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<Type, InstanceCreator<?>> val = (Map<Type, InstanceCreator<?>>) f.get(gsonBuilder);
    builder.registerTypeAdapterFactory(new CollectionTypeAdapterFactory(new ConstructorConstructor(val)));
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
    builder.registerTypeAdapterFactory(new CollectionTypeAdapterFactory(new ConstructorConstructor(new HashMap<Type, InstanceCreator<?>>())));
}

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转为了[],实验成功!

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

记录,分享,交流。

猜你喜欢

转载自blog.csdn.net/choimroc/article/details/89300883