gson的错误处理——让局部解析失败不会引起整体失败的反思

json处理库五花八门,本文适合使用gson的老铁。

gson也算是老牌json处理库了,在某些性能方面被弟弟们摩擦,不过因为兼容性不错(公司再用)的原因,还是要改改它的脾气。

问题:PHP同学过于奔放,什么乱七八糟字段都有,一会"null",一会"",一会int给你传个字符串,一会数组当字典,遇到过太多因为某个字段解析失败导致一个请求整体解析失败的问题了。

又不能打人家,只能自己不受影响了。

 

入口:fromJson("我是json", JsonBean.class)

gson方法不多,但一眼看下去看不到重点,索性直接对json解码,然后debug进去,fromJson调用如下:

      reader.peek();
      isEmpty = false;
      TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
      TypeAdapter<T> typeAdapter = getAdapter(typeToken);
      T object = typeAdapter.read(reader);
      return object;

 fromJson调用getAdapter去获取所传入类型JsonBean这个类所对应的类型适配器,然后用这个适配器去解析。

下面进入getAdapter:

  public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
    TypeAdapter<?> cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type);
    if (cached != null) {
      return (TypeAdapter<T>) cached;
    }

    Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get();
    boolean requiresThreadLocalCleanup = false;
    if (threadCalls == null) {
      threadCalls = new HashMap<TypeToken<?>, FutureTypeAdapter<?>>();
      calls.set(threadCalls);
      requiresThreadLocalCleanup = true;
    }

    // the key and value type parameters always agree
    FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type);
    if (ongoingCall != null) {
      return ongoingCall;
    }

    try {
      FutureTypeAdapter<T> call = new FutureTypeAdapter<T>();
      threadCalls.put(type, call);

      for (TypeAdapterFactory factory : factories) {
        TypeAdapter<T> candidate = factory.create(this, type);
        if (candidate != null) {
          call.setDelegate(candidate);
          typeTokenCache.put(type, candidate);
          return candidate;
        }
      }
      throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);
    } finally {
      threadCalls.remove(type);

      if (requiresThreadLocalCleanup) {
        calls.remove();
      }
    }
  }

方法开始,根据类型查找类型适配器,先从typeTokenCache查找,找不到就创建一个并扔到typeTokenCache里面。创建是由一组factories来创建的,逻辑是只要一个factory创建不为空,就把factory创建的类型适配器返回。

追踪factories的源头,发现原来他是在Gson的构造方法中初始化的,其中包含了大量的工厂:

    List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>();

    // ...

    factories.add(new ReflectiveTypeAdapterFactory(
        constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory));

    // ...

    this.factories = Collections.unmodifiableList(factories);

 从这里我们可以了解到:Gson在初始化时基本上为所有能用到的类型都创建了用来创建类型适配器的类型适配器工厂,这句有点绕,理解下。我们传入的class,就是根据某一个工厂创建的适配器去解析的。

那么是哪个适配器解析的我们的参数呢?在Gson#fromJson方法中,断点进入到typeAdapter.read中去,发现解析我们的适配器如下:

  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 {
        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;
    }

  }

这个适配器是ReflectiveTypeAdapterFactory的内部类。可以看到,这个类有个成员boundFields,包含了他所有的成员,在read方法中,遍历所有的成员,对其进行赋值,调用boundFields.read方法,boundField的实现为一个匿名类,在createBoundField方法中:

  private ReflectiveTypeAdapterFactory.BoundField createBoundField(
      final Gson context, final Field field, final String name,
      final TypeToken<?> fieldType, boolean serialize, boolean deserialize) {
    final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
    // special casing primitives here saves ~5% on Android...
    JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
    TypeAdapter<?> mapped = null;
    if (annotation != null) {
      mapped = jsonAdapterFactory.getTypeAdapter(
          constructorConstructor, context, fieldType, annotation);
    }
    final boolean jsonAdapterPresent = mapped != null;
    if (mapped == null) mapped = context.getAdapter(fieldType);

    final TypeAdapter<?> typeAdapter = mapped;
    return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
      @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
      @Override void write(JsonWriter writer, Object value)
          throws IOException, IllegalAccessException {
        Object fieldValue = field.get(value);
        TypeAdapter t = jsonAdapterPresent ? typeAdapter
            : new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType());
        t.write(writer, fieldValue);
      }
      @Override void read(JsonReader reader, Object value)
          throws IOException, IllegalAccessException {
        Object fieldValue = typeAdapter.read(reader);
        if (fieldValue != null || !isPrimitive) {
          field.set(value, fieldValue);
        }
      }
      @Override public boolean writeField(Object value) throws IOException, IllegalAccessException {
        if (!serialized) return false;
        Object fieldValue = field.get(value);
        return fieldValue != value; // avoid recursion for example for Throwable.cause
      }
    };
  }

在它的read方法中,首先根据字段的类型,调用另一个适配器,获取到的值,使用反射的方法set到对象上。

通过追踪了解到,这个read方法对应的位置如果是个对象,这个typeAdapter就会实例化为ReflectiveTypeAdapterFactory#Adapter,由此实现了内部类的层层嵌套。

 

到这里我们的结论:

所有的类型的解析,用的无非就是用的Gson构造方法中创建的那几个factory生成的适配器,只要我们按照类型去自己定义适配器,就能够改变指定的类型的处理行为。

 

上面看那么多,其实是今天遇到了一个问题,就是服务端在本是String类型的字段,下发了一个Map。

那我们就针对这个状况撸一个适配器,然后通过registerAdapter方法注册到Gson对象。

看看抛异常的地方,发生在这个方法JsonReader#nextString:

  public String nextString() throws IOException {
    int p = peeked;
    if (p == PEEKED_NONE) {
      p = doPeek();
    }
    String result;
    if (p == PEEKED_UNQUOTED) {
      result = nextUnquotedValue();
    } else if (p == PEEKED_SINGLE_QUOTED) {
      result = nextQuotedValue('\'');
    } else if (p == PEEKED_DOUBLE_QUOTED) {
      result = nextQuotedValue('"');
    } else if (p == PEEKED_BUFFERED) {
      result = peekedString;
      peekedString = null;
    } else if (p == PEEKED_LONG) {
      result = Long.toString(peekedLong);
    } else if (p == PEEKED_NUMBER) {
      result = new String(buffer, pos, peekedNumberLength);
      pos += peekedNumberLength;
    } else {
      throw new IllegalStateException("Expected a string but was " + peek() + locationString());
    }
    peeked = PEEKED_NONE;
    pathIndices[stackSize - 1]++;
    return result;
  }

如果peek到Map的时候能不走到else就可以不抛异常了,可惜的是Gson是final类型的,我们没法通过覆写newJsonReader的方法干预JsonReader的创建。那么我们就让他不进入nextString好了。

来到JsonReader#nextString的调用者,是TypeAdapters类的内部类:

  public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
    @Override
    public String read(JsonReader in) throws IOException {
      JsonToken peek = in.peek();
      if (peek == JsonToken.NULL) {
        in.nextNull();
        return null;
      }
      /* coerce booleans to strings for backwards compatibility */
      if (peek == JsonToken.BOOLEAN) {
        return Boolean.toString(in.nextBoolean());
      }
      return in.nextString();
    }
    @Override
    public void write(JsonWriter out, String value) throws IOException {
      out.value(value);
    }
  };
  

我们参考默认的String类型的适配器重写一个再注册到Gson就完事了。

代码:

    // 参考String类型的默认解析器实现TypeAdapters#STRING
    public static class StrongStringTypeAdapter extends TypeAdapter<String> {

        @Override
        public String read(JsonReader in) throws IOException {
            JsonToken peek = in.peek();
            if (peek == JsonToken.NULL) {
                in.nextNull();
                return null;
            }
            /* coerce booleans to strings for backwards compatibility */
            if (peek == JsonToken.BOOLEAN) {
                return Boolean.toString(in.nextBoolean());
            }

            // 跳过
            if (skipInvalidContent(in)){
                return null;
            }
            return in.nextString();
        }

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

    private static boolean skipInvalidContent(JsonReader in) throws IOException {
        boolean handled = false;
        JsonToken peek = in.peek();
        if (peek == JsonToken.BEGIN_OBJECT){
            in.beginObject();
            while (in.hasNext()){
                in.nextName();
                in.skipValue();
            }
            in.endObject();
            handled = true;
        }
        if (peek == JsonToken.BEGIN_ARRAY){
            in.beginArray();
            while (in.hasNext()){
                in.nextName();
                in.skipValue();
            }
            in.endArray();
            handled = true;
        }
        return handled;
    }

至此,PHP的屁股擦完了。

只是提供一个思路,后面的大家自己补充,毕竟授人以鱼不如授人以渔。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 你好,关于你的问题,我可以给予一些建议和解决方案。 首先,gson解析动态data失败整数类型成功是字符串类型的问题可能是由于数据格式不符合预期导致的。可以尝试检查数据的格式是否正确,例如整数类型是否被正确地表示为数字。 其次,也可以考虑在代码中针对这种情况进行处理,例如使用try-catch语句捕获解析异常并采取相应的处理措施。 需要注意的是,对于具体的代码实现问题,我可能需要更多的上下文信息才能给出更准确的解决方案。如果您能提供更多细节,我可以尽力为您提供帮助。 ### 回答2: gson解析动态data时,如果解析失败将整数类型转换为字符串类型的原因可能有多种。下面我将介绍一些可能的原因和解决方法。 首先,当使用gson解析动态data时,可能会遇到数据类型不匹配的情况。例如,在解析时期望获取一个整数类型数据,但是实际上data中的值可能是一个字符串类型。这样的情况可能导致解析失败。 其次,数据的动态性也可能导致解析失败。例如,data中的值在不同的情况下可能具有不同的数据类型,这就需要我们在解析时进行判断和处理。 解决这个问题的一种方法是在解析前先对数据进行类型判断,可以使用gson的JsonElement来判断data的类型。如果data是一个整数类型,那么可以将其直接解析为整数。如果data是一个字符串类型,那么可以将其解析为字符串。 此外,还可以使用gson的自定义解析器来解析动态data。自定义解析器可以根据data的不同数据类型进行相应的解析操作。 综上所述,gson解析动态data时失败整数类型而成功是字符串类型的原因可能是数据类型不匹配或数据的动态性导致的。我们可以通过类型判断和自定义解析器来解决这个问题。 ### 回答3: 在使用Gson解析动态data时,如果解析失败数据自身是整数类型,那么解析结果会是字符串类型。这是由于Gson解析数据时,会根据数据的实际类型进行相应的转换。 例如,假设动态data是一个包含整数类型数据的JSON字符串,我们可以通过以下方式使用Gson进行解析: ```java String jsonData = "{\"number\": 123}"; Gson gson = new Gson(); DataObject dataObject = gson.fromJson(jsonData, DataObject.class); ``` 在这个例子中,我们定义了一个DataObject类来表示解析后的数据: ```java public class DataObject { private Object number; // getter and setter methods } ``` 当我们调用`gson.fromJson()`方法来解析JSON字符串时,Gson会尝试将data中的`number`字段解析为整数类型,并存储在`DataObject`实例的`number`字段中。但是,如果解析失败Gson会将该字段作为字符串类型处理。 因此,如果解析成功,那么`dataObject.getNumber()`将返回一个整数类型的值123。但如果解析失败,那么`dataObject.getNumber()`将返回一个字符串类型的值"123"。 在使用Gson解析动态data时,需要注意数据类型的一致性,以避免因类型转换导致的错误结果。如果需要精确控制数据类型,可以根据实际情况在`DataObject`类中将`number`字段的类型定义为字符串类型,并手动进行类型转换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值