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上。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

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