Gson,不规范json的反序列化

Java基础

浏览数:257

2019-8-21

  Gson是Java或Android开发中常用的一个json解析库,尤其是在Android下基本上是必备的。但是做项目嘛,总会遇到各种奇葩的问题,这里所说的不规范json并不是说json格式不规范,毕竟格式都不规范的话就谈不上是个json串了,是不可能解析的。这里所谓的不规范json是在后台反回的json串能解析的情况下,我们的实体类不能很方便的接收它,看下面的情况(由此可见后台数据格式对前端的影响之大,有时候对后台友好的数据格式对前端来说并不友好反而会增加前端的工作量,尤其是像Java这种强类型语言来说,要想用的方便那就每一个字段都要严格限制它的数据类型):

[
    {
        "id": 1097320752316833800,
        "newsInfo": {
            "geolocation": {
                "lon": "0",
                "lat": "0"
            },
            "type": "5",
            "id": "1097320752316833794",
            "contentId": 1097320752316833800,
            "publishTime": "2019-02-18 10:23:49"
        },
        "freshnewsInfo": "",
        "activityContent": "",
        "specialInfo": "",
        "eventInfo": "",
        "contentType": "1"
    },
    {
        "id": 1097320752316833800,
        "newsInfo": "",
        "freshnewsInfo": {
            "sourceType": 2,
            "videoImgUrl": "",
            "name": "用户TrrLXe",
            "relatednewsId": "",
            "fileId": "",
            "imeiNo": "",
            "createTime": "2019-02-18 09:44:04",
            "geolocation": {
                "lon": 116.4835,
                "lat": 39.9235
            }
        },
        "activityContent": "",
        "specialInfo": "",
        "eventInfo": "",
        "contentType": "1",
        "sequence": "",
        "createTime": "2019-02-18 10:56:25",
        "showType": 1
    }
]
public class Content{
    private long id;
    private NewsInfoBean newsInfo;
    private FreshnewsInfoBean freshnewsInfo;
    private String activityContent;
    private EventInfoBean eventInfo;
    private SpecialInfoBean specialInfo;
    private int contentType;
    private String sequence;
    private String createTime;
    private String showType;
}

  上面的json串相当于一个List,每个Content中都有若干个*Info(newsInfo、SpecialInfo、eventInfo等)字段,其中只有一个*Info字段有值,其余没有值的理论上应该为null,但是后台返回的是空字符串,这样问题就大了,当为空字符串的时候怎么解析呢?空字符串是无法赋到一个实体对象的,对于Gson来说会报下面这样一个错误:

com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 26 path $.data[0].newsInfo

  意思是说要解析的newsInfo值应该是个Object类型,但现在却是一个String类型,所以解析失败了。好吧,这个问题跟后台人员反应让他们把空字符串转为null,但他们也很无奈,反复沟通后无果,最终还是把问题抛给了前端/(ㄒoㄒ)/~~。
  其实这个问题还是有不少解决方式的,比如改下实体结构:

public class Content{
    private long id;
    private Object newsInfo;
    private Object freshnewsInfo;
    private String activityContent;
    private Object eventInfo;
    private Object specialInfo;
    private int contentType;
    private String sequence;
    private String createTime;
    private String showType;
}

  将所有不能正常解析的字段都改用Object类型接收,然后再根据需要解析Object到具体的实体结构;或者是使用Gson框架中的JsonParse类解析返回的json数据,这两种方式都可以,但对于整个项目来说,有不少接口是这种不规范的格式,每个都这样做的话无疑增加了成本和复杂度,此外还有一个更令人抓狂的问题,而且我相信很多人都碰到过。
  还是上面的json串,在newsInfo和refreshInfo字段中,有个geolocation字段:

"newsInfo": {
    "geolocation": {
        "lon": "0",
        "lat": "0"
    },
    "type": "5",
    "id": "1097320752316833794",
    "contentId": 1097320752316833800,
    "publishTime": "2019-02-18 10:23:49"
}

  对于后台来说,从不同的表中查出的字段放到一个单独的对象实体中没问题,但对于前端来说,放到和其他内容一个级别下才是最方便的,比如我们期望它的解析实体是长这样的:

public class NewsInfo {
    private double lon;
    private double lat;
    private int type;
    private long id;
    private long contentId;
    private String publishTime;
}

  如果这个json串中还有地址信息、作者信息话那有可能会是这样的:

"newsInfo": {
    "geolocation": {
        "lon": "0",
        "lat": "0"
    },
    "type": "5",
    "id": "1097320752316833794",
    "contentId": 1097320752316833800,
    "publishTime": "2019-02-18 10:23:49"
    "address": {
        "country":"CN",
        "countryNO":"86",
        "city":"北京"
    },
    "author": {
        "nickName":"xiaxia",
        "icon":"",
        "registTime":"2018-12-18 09:33:50",
        "type":"1",
    }
}

  而我需要的只是address中的city或是author中的nickName和icon字段,所以我们期望它的解析实体是这样:

public class NewsInfo {
    private double lon;
    private double lat;
    private String city;
    private String nickName;
    private String icon;
    private int type;
    private long id;
    private long contentId;
    private String publishTime;
}

  这种实体结构,使用Gson是无法正常解析上面的json串的,但Gson框架提供了强大的可扩展功能,我们完全可以自定义解析方式来达到我们的需求。

Gson序列化和反序列化的方式

  自定义json解析,Gson框架为我们提供了以下几种方式:

  • 继承TypeAdapter类

  需要重写read(JsonReader in)反序列化方法和write(JsonWriter out, Object value)序列化方法,扩展性低,只能针对一种类型进行处理。

  • 实现TypeAdapterFactory接口

  需要重写create(Gson gson, TypeToken<T> type)方法,并返回一个TypeAdapter对象,扩展性高,可通过判断类型来创建对应的TypeAdapter

  • 实现JsonDeserializer或JsonSerializer接口

  扩展性高,将序列化和反序列化分开,可只针对其中一种进行自定义

  这3种方法本质上都是创建一个新的TypeAdapter,其中前两种需要重写序列化和反序列化方法,第三种是将序列化和反序列化方法分开了,可以单独实现一种,我们这里使用第三种方式,实现JsonDeserializer接口,这样的话我们就只自定义了反序列化方法,序列化方法仍然走Gson框架本身。

Gson反序列化过程

  在Gson框架中,反序列化的过程是这样的:
1. 解析json串后得到一个JsonReader;
2. 通过hasNext()迭代JsonReader获取当前json串的位置;
3. 再通过peek()方法判断该位置下的类型(BEGIN_OBJECT、BEGIN_ARRAY、END_ARRAY、END_OBJECT、NAME等,具体可看JsonToken类);
4. 如果该类型是个NAME就说明遇到一个字段,那么通过nextName()方法获取该字段的值,然后执行第5步骤,否则继续执行第3步骤;
5. 如果实体对象中有该字段,那再获取该字段的数据类型;
6. 通过判断该数据类型来决定接下将会通过JsonReader的哪个方法(nextString()、beginObject()、beginArray)获取到字段的值。

  以上就是Gson中反序列化的过程,我们上面发生Gson报错的地方就是在第6步发生的,由于newsInfo字段是个Object,但在JsonReader中却是一个String,所以对于一个String类型来说如果执行beginObject()方法就会报错。这里也是我不明白的地方,对于Gson框架来说为什么不通过peek()方法先判断一下类型进行容错然后再获取值呢。

自定义反序列化过程

  Gson的反序列化过程是顺序执行的,从json串的头开始一直迭代到尾遇到什么就获取什么,可谓是行云流水一气呵成。接下来针对我们的需求重新定义一下反序列化过程,主要有三个目标:

1.摆脱后台数据格式对前端的影响,前端实体类不一定完全按照后台的格式写
2.增加json反序列化的容错能力
3.获取json中不同层级的值

1. 实现JsonDeserializer接口,并重写其中方法;
2. 获取实体类的所有字段,并获取字段的注解信息;
3. 获取JsonElement中的members字段,这是一个LinkedTreeMap类型,存储了解析json后的层级结构;
4. 遍历字段,获取members中对应字段的值,该值也是一个JsonElement类型;
5. 判断字段类型和该JsonElement类型是否一致,避免类型不同而在赋值时报错;
6. 如果该类型是基本类型则进行赋值操作,否则该类型可能是数组或实体类,那就重复执行第四步骤;

  很显然,该反序列化过程不是顺序执行的,而是根据json的层次结构进行查找的,虽然速度上落后了但灵活性却提高了,可以根据实体字段随意查找想要的值。

使用方式

@JsonAdapter(value = IllegalJsonDeserializer.class)
public class Content{
    private long id;
    @Select(value = "address.city")
    private String city;
    private NewsInfoBean newsInfo;
    private FreshnewsInfoBean freshnewsInfo;
    private String activityContent;
    private EventInfoBean eventInfo;
    private SpecialInfoBean specialInfo;
    private int contentType;
    private String sequence;
    private String createTime;
    private String showType;
}
  1. 在需要反序列化的类或字段上直接使用@JsonAdapter(value = IllegalJsonDeserializer.class)注解
  2. 如果需要获取其他层级的值,可在字段上声明@Select(value = "address.city"),其中value的值用.作为分隔符作为不同层级的划分,比如上面的值就是说明要将address下的city字段赋值到Content类中的city字段

  除此之外,该反序列方法还兼容了Gson本身的注解,完全可以使用Gson的原生注解方法。源码已放在github上,如果有遇到同样问题或者对此感兴趣朋友的可以看一下。

https://github.com/chengzhicao/illegal-json

作者:caochengzhi