关于 Gson 容错的思考

本文探讨了在遇到Gson反序列化时遇到的问题,包括后端返回类型不一致和数据请求失败时返回List。通过分析Gson源码,了解其工作原理,并提出自定义TypeAdapter实现容错机制,特别介绍了如何处理int与Integer转换异常以及Object与List之间的转换问题。同时,提出了两种解决方案:代码插桩和修改ReflectiveTypeAdapterFactory,建议使用代码插桩以避免潜在风险。

最近项目中遇到了两个 Json 解析的坑,一个是后端没有按照约定返回对应类型的字段,文档说明写了 int ,但是它直接返回了 String 类型;另外一个数据请求成功后, data 是一个 Object 类型,但是如果数据请求失败,后端直接返回了 List

因为这两个坑,后端周末紧急加班去发版本, 不过就勾起了我对 Gson 容错的方案思考

Gson 源码的分析

要解决业务方的需求,客户端这边想法子做一定的 Json 容错,那么就必须去了解一下 Gson 的源码了

先看下 Gson 的日常用法:

Gson gson = new Gson();
//反序列化得到数据bean
DataBean data = gson.fromJson(jsonString,DataBean.class);

Gson 是如何将 Json 反序列化的?答案是通过 TypeAdapter

关于 TypeAdapter ,可以看一下这篇文章:你真的会用Gson吗? 整个系列看完,Gson的基本用法就都掌握了

Gson 内部实现了大量的 TypeAdapter ,在其构造方法里面,就将全部的 TypeAdapter 存储起来:

 Gson(final Excluder excluder, final FieldNamingStrategy fieldNamingStrategy,
      final Map<Type, InstanceCreator<?>> instanceCreators, boolean serializeNulls,
      boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe,
      boolean prettyPrinting, boolean lenient, boolean serializeSpecialFloatingPointValues,
      LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle,
      int timeStyle, List<TypeAdapterFactory> builderFactories,
      List<TypeAdapterFactory> builderHierarchyFactories,
      List<TypeAdapterFactory> factoriesToBeAdded) {
    //省去部分代码
    List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>();

    // built-in type adapters that cannot be overridden
    factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
    factories.add(ObjectTypeAdapter.FACTORY);

    // the excluder must precede all adapters that handle user-defined types
    factories.add(excluder);

    //重点:添加自定义的 TypeAdapter
    factories.addAll(factoriesToBeAdded);

    // type adapters for basic platform types
    factories.add(TypeAdapters.STRING_FACTORY);
    factories.add(TypeAdapters.INTEGER_FACTORY);
    factories.add(TypeAdapters.BOOLEAN_FACTORY);
    factories.add(TypeAdapters.BYTE_FACTORY);
    factories.add(TypeAdapters.SHORT_FACTORY);
    TypeAdapter<Number> longAdapter = longAdapter(longSerializationPolicy);
    factories.add(TypeAdapters.newFactory(long.class, Long.class, longAdapter));
    factories.add(TypeAdapters.newFactory(double.class, Double.class,
            doubleAdapter(serializeSpecialFloatingPointValues)));
    factories.add(TypeAdapters.newFactory(float.class, Float.class,
            floatAdapter(serializeSpecialFloatingPointValues)));
    factories.add(TypeAdapters.NUMBER_FACTORY);
    factories.add(TypeAdapters.ATOMIC_INTEGER_FACTORY);
    factories.add(TypeAdapters.ATOMIC_BOOLEAN_FACTORY);
    factories.add(TypeAdapters.newFactory(AtomicLong.class, atomicLongAdapter(longAdapter)));
    factories.add(TypeAdapters.newFactory(AtomicLongArray.class, atomicLongArrayAdapter(longAdapter)));
    factories.add(TypeAdapters.ATOMIC_INTEGER_ARRAY_FACTORY);
    factories.add(TypeAdapters.CHARACTER_FACTORY);
    factories.add(TypeAdapters.STRING_BUILDER_FACTORY);
    factories.add(TypeAdapters.STRING_BUFFER_FACTORY);
    factories.add(TypeAdapters.newFactory(BigDecimal.class, TypeAdapters.BIG_DECIMAL));
    factories.add(TypeAdapters.newFactory(BigInteger.class, TypeAdapters.BIG_INTEGER));
    factories.add(TypeAdapters.URL_FACTORY);
    factories.add(TypeAdapters.URI_FACTORY);
    factories.add(TypeAdapters.UUID_FACTORY);
    factories.add(TypeAdapters.CURRENCY_FACTORY);
    factories.add(TypeAdapters.LOCALE_FACTORY);
    factories.add(TypeAdapters.INET_ADDRESS_FACTORY);
    factories.add(TypeAdapters.BIT_SET_FACTORY);
    factories.add(DateTypeAdapter.FACTORY);
    factories.add(TypeAdapters.CALENDAR_FACTORY);
    factories.add(TimeTypeAdapter.FACTORY);
    factories.add(SqlDateTypeAdapter.FACTORY);
    factories.add(TypeAdapters.TIMESTAMP_FACTORY);
    factories.add(ArrayTypeAdapter.FACTORY);
    factories.add(TypeAdapters.CLASS_FACTORY);
    // 重点:CollectionTypeAdapterFactory,用于处理集合的
    factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
    factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization));
    this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor);
    factories.add(jsonAdapterFactory);
    factories.add(TypeAdapters.ENUM_FACTORY);
    // 重点:ReflectiveTypeAdapterFactor,用于反射生产数据bean实例
    factories.add(new ReflectiveTypeAdapterFactory(
        constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory));
		//重点:unmodifiableList,一个不可修改的List
    this.factories = Collections.unmodifiableList(factories);
  }

那么 Gson 是怎么知道应该使用哪一个 TypeAdapter 呢?Gson 里面有一个 getAdapter 方法,通过参数 type 找到对应的 adapter

 public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
    //省去部分代码
    try {
      FutureTypeAdapter<T> call = new FutureTypeAdapter<T>();
      threadCalls.put(type, call);
			//这里遍历去找对应 type 的 adapter
      for (TypeAdapterFactory factory : factories) {
        TypeAdapter<T> candidate = factory.create(this, type);
        if (candidate != null) {
          call.setDelegate(candidate);
          typeTokenCache.put(type, candidate);
          //找到了就立即返回
          return candidate;
        }
      }
     //省去部分代码
  }

还记得上面的 TypeAdapter 的添加顺序吗?

在这里插入图片描述

那么我们能否这么做?参考 Gson 里面的 TypeAdapter ,添加我们需要的容错机制,作为自定义 TypeAdapter 添加到 Gson 里面?

自定义容错的 TypeAdapter

先来看一下 Gson 源码里的内置的一个 TypeAdapter ,比分说用于解析 intInteger 的:

public static final TypeAdapterFactory INTEGER_FACTORY
      //用于解析 int 和 Integer
      = newFactory(int.class, Integer.class, INTEGER);

public static final TypeAdapter<Number> INTEGER = new TypeAdapter<Number>() {
    @Override
    public Number read(JsonReader in) throws IOException {
      if (in.peek() == JsonToken.NULL) {
        in.nextNull();
        return null;
      }
      try {
        return in.nextInt();
      } catch (NumberFormatException e) {
        //这里抛出了json解析异常
        throw new JsonSyntaxException(e);
      }
    }
    @Override
    public void write(JsonWriter out, Number value) throws IOException {
      out.value(value);
    }
  };

现在来参考上面的代码,写一个针对解析 intInteger 解析异常的 TypeAdapter

//使用Integer可以让getAdapter更加精准找到该adapter
public class IntegerTypeAdapter extends TypeAdapter<Integer> {

    @Override
    public Integer read(JsonReader in) throws IOException {
        //获取一个JsonToken而不会消耗它,JsonToken为这次读取的数据基本类型枚举
        JsonToken peek = in.peek();
        if (peek == JsonToken.NULL) {
          //next方法为读取当前数据并将指针指向下个数据
           in.nextNull();
           return null;
        }
        if (peek == JsonToken.NUMBER) {
          try {
                //next方法为读取当前数据并将指针指向下个数据
                return in.nextInt();
                } catch (NumberFormatException e) {
                  //JsonToken.NUMBER 对应int、long和double这3个类型
                  //有可能会抛出这个异常
                  return 0;
                }
        }
        if (peek == JsonToken.STRING) {
           String result = in.nextString();
           if (TextUtils.isEmpty(result)) {
    					 return 0;
					}
           try {
                //next方法为读取当前数据并将指针指向下个数据
                return Integer.parseInt(result);
                } catch (NumberFormatException e) {
                  //这里可以添加埋点上报或者将异常打印出来
                  return 0;
                }
        }
        //设置为跳过读取解析当前数据并返回0
        in.skipValue();
        return 0;
        }
    }

    @Override
    public void write(JsonWriter out, Integer value) throws IOException {
        out.value(value);
    }
}

ok,现在来测试一下这个新的 TypeAdapter

Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class
                                                            , new IntegerTypeAdapter()))
        .create();

Log.d("json1",gson.fromJson("\"\"",int.class) + "");//空字符串
Log.d("json2",gson.fromJson("10.1f",Integer.class) + "");//float类型
Log.d("json3",gson.fromJson("202101311934L",int.class) + "");//long类型
Log.d("json4",gson.fromJson("\"NULL\"",Integer.class) + "");//奇怪的字符串

看一下运行结果:

在这里插入图片描述

可以看到, Gson 运行没有报错并返回了默认值 0 ,这说明了思路是对的,那么现在要解决第二个问题了

data 是一个 Object 类型,但是如果数据请求失败,后端直接返回了 List

要解决这个问题,那么就需要先找到 Gson 是怎么解析 Object 类型的数据 Bean 了,我们都知道 Gsonjson 反序列化为 数据Bean 是通过反射的,那么 Gson 肯定有一个基于反射的 TypeAdapter ,对 Gson 的构造方法还有印象的话,应该还记得 Gson 构造方法里面有一个 ReflectiveTypeAdapterFactory

ReflectiveTypeAdapterFactory

在 Gson 添加 TypeAdapter 时,ReflectiveTypeAdapterFactory 是最后才添加进去的,因为在基本数据类型都不匹配的情况下,我们可以将其视为一个 数据Bean,那么就轮到 ReflectiveTypeAdapterFactory 来处理了,因为它可以通过反射来创建类的实例以及给类的实例赋值,这一点也清楚写在 ReflectiveTypeAdapterFactory 的注释里面

在这里插入图片描述

我们来简单分析一下它是怎么做的吧,ReflectiveTypeAdapterFactory 是一个工厂,它创建的 TypeAdapterReflectiveTypeAdapterFactory 里面的一个内部类:

public class ReflectiveTypeAdapter<T> extends TypeAdapter<T> {  
  
    //省去部分代码 
  
    @Override
    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
        Class<? super T> raw = type.getRawType();
        if (!Object.class.isAssignableFrom(raw)) {
          return null; // it's a primitive!
        }
        ObjectConstructor<T> constructor = constructorConstructor.get(type);
        //这里创建真正的 TypeAdapter
        return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
      }
		
    //省去部分代码
    
    //ReflectiveTypeAdapterFactory的内部类,用于解析json并使用反射构建类
    public static final class Adapter<T> extends TypeAdapter<T> {
        private final ObjectConstructor<T> constructor;
        private final Map<String, BoundField> boundFields;

        Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
          this.constructor = constructor;
          this.boundFields = boundFields;
        }

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

          T instance = constructor.construct();

          try {
            //这里开始使用 Object 进行反序列化了
            //如果此时后端返回的是 List 类型数据,那么就会抛出异常并且App crash
            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);
              }
            }
          } catch (IllegalStateException e) {
            throw new JsonSyntaxException(e);
          } catch (IllegalAccessException e) {
            throw new AssertionError(e);
          }
          in.endObject();
          return instance;
        }

        @Override 
        public void write(JsonWriter out, T value) throws IOException {
          //这里处理空数据
          if (value == null) {
            out.nullValue();
            return;
          }

          out.beginObject();
          try {
            for (BoundField boundField : boundFields.values()) {
              if (boundField.writeField(value)) {
                out.name(boundField.name);
                boundField.write(out, value);
              }
            }
          } catch (IllegalAccessException e) {
            throw new AssertionError(e);
          }
          out.endObject();
        }
      }
}

看到这里,其实就明白了,解决方案很简单,在 in.beginObject(); 这行代码执行之前,判断一下输入的 json 类型即可:

 if (in.peek() == JsonToken.NULL) {
        in.nextNull();
        return null;
      }

//======插入容错的代码的分割线==================
 if (in.peek() != JsonToken.BEGIN_OBJECT) {
    //当前不是 Object 类型数据,跳过解析该数据
    in.skipValue();
    return null;
}
//======插入容错的代码的分割线==================
T instance = constructor.construct();

这样子就可以解决问题了,不过 Gson 源码没有做这个判断,那应该怎么做呢?其实有两个法子

一个是使用代码插桩,在编程过程中在 ReflectiveTypeAdapterFactory 里面插入上面这一段代码

可以参考这个:GsonPlugin

另一个就不怎么优雅,也比较危险,直接将 ReflectiveTypeAdapterFactory 相关代码拷贝出来,然后加上上面那一段代码

再将我们修改后的 ReflectiveTypeAdapterFactory 插入在 Gson 缓存的 TypeAdapter 列表(这是一个不可修改的List,就算反射获取到该对象也无法进行 add 或者 remove,因此只能加入到自定义 TypeAdapter 里面)

同时要想法子不影响其他类型的反序列化,这就是为什么说这种做法很危险

因为这么做,很容易就导致整个 Gson 的 反射序列化实例对象出问题 !

因此,最好使用 代码插桩 的方法来完成 Gson 容错处理

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值