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 {

        @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() {
                        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 INTEGER = new TypeAdapter() {
        @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 LONG = new TypeAdapter() {
        @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 data = (List) factories.get(gson);
            List 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上。

你可能感兴趣的:(Gson针对API返回字段类型不确定的解决办法)