Gson针对API返回字段类型不确定的解决办法

遇到问题

最近得到用户反馈,有些界面请求数据失败,调试接口发现,是后台返回的类型不确定导致的,例如:
这是一段我们需要的正常json:

{
    "id":1,
    "number":100000000,
    "user":{
        "name":"aoteman",
        "userId":110
    }
}

但是后台有时会返回这样一段json:

{
    "id":"",
    "number":"",
    "user":""
}

可以发现,本来是int型的id,long型的number和Object的类型的user都变成了空字符串。只要是返回结果为null的,都有可能返回成空字符串,可以猜到后台可能是使用了PHP这种弱类型语言,并且没有对字段做类型校验。这本来应该是后台的锅,但是部门之间沟通困难,只能先我们客户端解决了。

解决方法

Gson在GsonBuilder提供了registerTypeAdapter这个方法,让我们对特定的模型进行自定义序列化和反序列化。这里提供了俩种序列化和反序列化的方式:

  • 第一种是继承自JsonSerializer和JsonDeserializer接口,我们主要看JsonDeserializer,代码如下:
public class Deserializer implements JsonDeserializer<Test> {

        @Override
        public Test deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
                throws JsonParseException {
            final JsonObject jsonObject = json.getAsJsonObject();
            final JsonElement jsonId = jsonObject.get("id");
            final JsonElement jsonNumber = jsonObject.get("number");
            final Test test = new Test();
            try {
                test.setId(jsonId.getAsInt());
            }catch (Exception e){
            //id不是int型的时候,捕获异常,并且设置id的值为0
                test.setId(0);
            }
            try {
                test.setNumber(jsonNumber.getAsLong());
            }catch (Exception e){
                test.setNumber(0);
            }
            return test;
        }
    }

这种方式类似于查字典,gson会把json先转换成JsonElement的结构,JsonElement有4种子类,JsonObject(对象结构)、JsonArray(数组结构)、JsonPrimitive(基本类型)、JsonNull。
JsonObject内部其实维护了一个HashMap,jsonObject.get("id");其实就是查字段。这种方法对性能的消耗比较大,因为它需要先把流数据转换成JsonElement的结构对象,这样会产生更大的内存消耗和运行时间。

  • 第二种是继承自TypeAdapter,代码如下:
new GsonBuilder().registerTypeAdapter(Test.class,
                    new TypeAdapter<Test>() {
                        public Test read(JsonReader in) throws IOException {
                            if (in.peek() == JsonToken.NULL) {
                                in.nextNull();
                                return null;
                            }
                            in.beginObject();
                            Test test = new Test();
                            while (in.hasNext()) {
                                switch (in.nextName()) {
                                    case "id":
                                        try {
                                            test.setId(in.nextInt());
                                        } catch (Exception e) {
                                            in.nextString();
                                            test.setId(0);
                                        }
                                        break;
                                    case "number":
                                        try {
                                            test.setNumber(in.nextLong());
                                        } catch (Exception e) {
                                            in.nextString();
                                            test.setNumber(0);
                                        }
                                        break;
                                }
                            }
                            return test;
                        }

                        public void write(JsonWriter out, Test src) throws IOException {
                            if (src == null) {
                                out.nullValue();
                                return;
                            }
                        }
                    })

这种方式相比JsonElement更加高效,因为它直接是用流来解析数据,去掉了JsonElement这个中间件,它流式的API相比于第一种的树形解析API将会更加高效。

那么问题的解决办法可以这样,如下代码:

//int类型的解析器
private static 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) {
            //这里解析int出错,那么捕获异常并且返回默认值,因为nextInt出错中断了方法,没有完成位移,所以调用nextString()方法完成位移。
                in.nextString();
                return 0;
            }
        }

        @Override
        public void write(JsonWriter out, Number value) throws IOException {
            out.value(value);
        }
    };
    
    private static TypeAdapter<Number> LONG = new TypeAdapter<Number>() {
        @Override
        public Number read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) {
                in.nextNull();
                return null;
            }
            //这里同
            try {
                return in.nextLong();
            } catch (Exception e) {
                in.nextString();
            }
            return 0;
        }

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

        }
    };
    
    Gson gson = new GsonBuilder()
                .registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
                .registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
                .create();

以上代码可以解决int和long类型的返回空字符串问题。但是自定义类型怎么办,不可能把每种类型都注册进来,我们先看看Gson是怎么做的,查看Gson的构造方法,发现下面一段代码:

factories.add(new ReflectiveTypeAdapterFactory(
        constructorConstructor, fieldNamingPolicy, excluder));

查看ReflectiveTypeAdapterFactory内容发现,这个是对所有用户定义类型的解析器。修改问题主要定位在ReflectiveTypeAdapterFactory类中的Adapter的read方法。我们只要修改read方法就可以解决问题:

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

            T instance = constructor.construct();
            //这里对beginObject进行异常捕获,如果不是object,说明可能是"",直接返回null,不中断解析
            try {
                in.beginObject();
            } catch (Exception e) {
                in.nextString();
                return null;
            }
            try {
                int count = 0;
                while (in.hasNext()) {
                    count++;
                    String name = in.nextName();
                    BoundField field = boundFields.get(name);
                    if (field == null || !field.deserialized) {
                        in.skipValue();
                    } else {
                        field.read(in, instance);
                    }
                }
                if (count == 0) return null;
            } catch (IllegalStateException e) {
                throw new JsonSyntaxException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            }
            in.endObject();
            return instance;
        }

ReflectiveTypeAdapterFactory是个final类不能继承,并且构造方法有几个重要参数从Gson传入,所以我们只能把ReflectiveTypeAdapterFactory复制一份出来修改,并且利用反射修改Gson,吧修改的类替换掉原来的,代码如下:

public static Gson buildGson() {
        Gson gson = new GsonBuilder().registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
                .registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
                .create();
        try {
            Field field = gson.getClass().getDeclaredField("constructorConstructor");
            field.setAccessible(true);
            ConstructorConstructor constructorConstructor = (ConstructorConstructor) field.get(gson);
            Field factories = gson.getClass().getDeclaredField("factories");
            factories.setAccessible(true);
            List<TypeAdapterFactory> data = (List<TypeAdapterFactory>) factories.get(gson);
            List<TypeAdapterFactory> newData = new ArrayList<>(data);
            newData.remove(data.size() - 1);
            newData.add(new MyReflectiveTypeAdapterFactory(constructorConstructor, FieldNamingPolicy.IDENTITY, Excluder.DEFAULT));
            newData = Collections.unmodifiableList(newData);
            factories.set(gson, newData);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return gson;
    }

问题到此得到解决,因为项目很少用到float、byte等类型的字段,所以就没有适配,如果有需要也可以通过以上方式解决。
示例代码托管在github上。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.概述2.Gson的目标3.Gson的性能和扩展性4.Gson的使用者5.如何使用Gson 通过Maven来使用...
    人失格阅读 14,344评论 2 18
  • 《牯岭街少年杀人事件》,很早就知道这部评价极高的台湾电影,却由于其四个多小时的时长一直没心理准备打开看。直到过年的...
    Shou撕鬼子阅读 260评论 0 1
  • 墨西哥城攻略 对墨西哥的印象以前都是停留在美国电影中被嘲讽腐败无能的墨西哥警察,墨西哥毒贩,还有就是仙人掌。这次我...
    等雨季阅读 292评论 3 2
  • 8月14日是爸爸的生日,今天开始,爸爸就正式29岁了。昨晚给羊爸爸和他同学胖子一起给他们过了一个生日。因为小马...
    碧海颖天阅读 291评论 0 0
  • 时至初夏暮春交接,寒暑阴晴未稳。无道愚人坐定无着,欲静不能,遂解衣更履,悠悠然进于南山。 苍穹无染,澄空一...
    巴波阅读 205评论 0 0