[Gson]Gson 解析 Json 容错策略-前沿热点
2023-03-31 04:59:58 来源:腾讯云

一. 序章

文章评论里后台有一些小伙伴,针对具体数据容错的场景,提出了具体的问题。今天就在这篇文章里统一解答,并且给出解决方案。

二. GSON 数据容错实例

就像前文中介绍的一样,GSON 已经提供了一些简单的注解,去做数据的容错处理。更复杂的操作,就需要用到 TypeAdapter 了,需要注意的是,一旦上了 TypeAdapter 之后,注解的配置就会失效。


(资料图)

2.1 什么是 TypeAdapter

TypeAdapter 是 GSON 2.1 版本开始支持的一个抽象类,用于接管某些类型的序列化和反序列化。TypeAdapter 最重要的两个方法就是 write()read(),它们分别接管了序列化和反序列化的具体过程。

如果想单独接管序列化或反序列化的某一个过程,可以使用 JsonSerializer 和 JsonDeserializer 这两个接口,它们组合起来的效果和 TypeAdapter 类似,但是其内部实现是不同的。

简单来说,TypeAdapter 是支持流的,所以它比较省内存,但是使用起来有些不方便。而 JsonSerializer 和 JsonDeserializer 是将数据都读到内存中再进行操作,会比 TypeAdapter 更费内存,但是 API 使用起来更清晰一些。

虽然 TypeAdapter 更省内存,但是通常我们业务接口所使用的那点数据量,所占用的内存其实影响不大,可以忽略不计。

因为 TypeAdapter、JsonSerializer 以及 JsonDeserializer 都需要配合 GsonBuilder.registerTypeAdapter()方法,所以在本文中,此种接管方式,统称为 TypeAdapter 接管。

2.2 空字符串转 0

对于一些强转有效的类型转换,GSON 本身是有一些默认的容错机制的。比如:将字符串 “18” 转换成 Java 中整型的 18,这是被默认支持的。

例如我有一个记录用户信息的 User 类。

class User{    var name = ""    var age = 0    override fun toString(): String {        return """            {                "name":"${name}",                "age":${age}            }        """.trimIndent()    }}

User 类中包含 nameage两个字段,其中 age对应的 JSON 类型,可以是 18也可以是 "18",这都是允许的。

{"name":"承香墨影","age":18 // "age":"18"}

那假如服务端说,这个用户没有填年龄的信息,所以直接返回了一个空串 "",那这个时候客户端用 Gson 解析就悲剧了。

这当然是服务端的问题,如果数据明确为 Int 类型,那么就算是默认值也应该是 0 或者 -1。

但遇到这样的情况,你还用默认的 GSON 策略去解析,你将得到一个 Crash。

Caused by: com.google.gson.JsonSyntaxException: - java.lang.NumberFormatException: --empty String

没有一点意外也没有一点惊喜的 Crash 了,那接下来看看如何解决这样的数据容错问题?

因为这里的场景中,只需要反序列化的操作,所以我们实现 JsonDeserializer 接口即可,接管的是 Int 类型。直接上例子吧。

class IntDefaut0Adapter : JsonDeserializer {    override fun deserialize(json: JsonElement?,                              typeOfT: Type?,                              context: JsonDeserializationContext?): Int {        if (json?.getAsString().equals("")) {            return 0        }        try {            return json!!.getAsInt()        } catch (e: NumberFormatException) {            return 0        }    }}fun intDefault0(){    val jsonStr = """        {            "name":"承香墨影",            "age":""        }    """.trimIndent()    val user = GsonBuilder()            .registerTypeAdapter(                    Int::class.java,                    IntDefaut0Adapter())            .create()            .fromJson(jsonStr,User::class.java)    Log.i("cxmydev","user: ${user.toString()}")}

在 IntDefaut0Adapter 中,首先判断数据字符串是否为空字符串 "",如果是则直接返回 0,否则将其按 Int 类型解析。在这个例子中,将整型 0 作为一个异常参数进行处理。

2.3 null、[]、List 转 List

还有一些小伙伴比较关心的,对于 JSONObject 和 JSONArray 兼容的问题。

例如需要返回一个 List,翻译成 JSON 数据就应该是方括号 []包裹的 JSONArray。但是在列表为空的时候,服务端返回的数据,什么情况都有可能。

{"name":"承香墨影","languages":["EN","CN"] // 理想的数据// "languages":""// "languages":null// "languages":{}}

例子的 JSON 中,languages字段表示当前用户所掌握的语言。当语言字段没有被设置的时候,服务端返回的数据不一致,如何兼容呢?

我们在原本的 User 类中,增加一个 languages 的字段,类型为 ArrayList。

var languages = ArrayList()

在 Java 中,列表集合都会实现 List 接口,所以我们在实现 JsonDeserializer 的时候,解析拦截的应该是 List。

在这个情况下,可以使用 JsonElement 的 isJsonArray()方法,判断当前是否是一个合法的 JSONArray 的数组,一旦不正确,就直接返回一个空的集合即可。

class ArraySecurityAdapter:JsonDeserializer>{    override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): List<*> {              if(json.isJsonArray()){            val newGson = Gson()            return newGson.fromJson(json, typeOfT)        }else{            return Collections.EMPTY_LIST        }    }}fun listDefaultEmpty(){    val jsonStr = """        {            "name":"承香墨影",            "age":"18",            "languages":{}        }    """.trimIndent()    val user = GsonBuilder()            .registerTypeHierarchyAdapter(                    List::class.java,                    ArraySecurityAdapter())            .create()            .fromJson(jsonStr,User::class.java)    Log.i("cxmydev","user: ${user.toString()}")}

其核心就是 isJsonArray()方法,判断当前是否是一个 JSONArray,如果是,再具体解析即可。到这一步就很灵活了,你可以直接用 Gson 将数据反序列化成一个 List,也可以将通过一个 for 循环将其中的每一项单独反序列化。

需要注意的是,如果依然想用 Gson 来解析,需要重新创建一个新的 Gson 对象,不可以直接复用 JsonDeserializationContext,否则会造成递归调用。

另外还有一个细节,在这个例子中,调用的是 registerTypeHierarchyAdapter()方法来注册 TypeAdapter,它和我们前面介绍的 registerTypeAdapter()有什么区别呢?

通常我们会根据不同的场景,选择不同数据结构实现的集合类,例如 ArrayList 或者 LinkedList。但是 registerTypeAdapter()方法,要求我们传递一个明确的类型,也就是说它不支持继承,而 registerTypeHierarchyAdapter()则可以支持继承。

我们想用 List 来替代所有的 List 子类,就需要使用 registerTypeHierarchyAdapter()方法,或者我们的 Java Bean 中,只使用 List。这两种情况都是可以的。

2.4 保留原 Json 字符串

看到这个小标题,可能会有疑问,保留原 Json 字符串是一个什么情况?得到的 Json 数据,本身就是一个字符串,且挺我细细说来。

举个例子,前面定义的 User 类,需要存到 SQLite 数据库中,语言(languages)字段也是需要存储的。说到 SQLite,当然优先使用一些开源的 ORM 框架了,而不少优秀的 ORM-SQLite 框架,都通过外键的形式支持了一对多的存储。例如一篇文章对应多条评论,一条用户信息对应对应多条语言信息。

这种场景下我们当然可以使用 ORM 框架本身提供的一对多的存储形式。但是如果像现在的例子中,只是简单的存储一些有限的数据,例如用户会的语言(languages),这种简单的有限数据,用外键有一些偏重了。

此时我们就想,要是可以直接在 SQLite 中存储 languages 字段的 JSON,将其当成一个字符串去存储,是不是就简单了?把一个多级的结构拉平成一级,剩下的只需要扩展出一个反序列化的方法,对业务来说,这些操作都是透明的。

那拍脑袋想,如果 Gson 有简单的容错,那我们将这个解析的字段类型定义成 String,是不是就可以做到了?

@SerializedName("languages")var languageStr = ""

很遗憾,这并没有办法做到,如果你这样使用,你将得到一个 IllegalStateException 的异常。

Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 4 column 18 path $.languages

之所以会出现这样的情况,简单来说,虽然 deserialize()方法传递的参数都是 JsonElement,但是 JsonElement 只是一个抽象类,最终会根据数据的情况,转换成它的几个实现类的其中之一,这些实现类都是 final class,分别是 JsonObject、JsonArray、JsonPrimitive、JsonNull,这些从命名上就很好理解了,它们代表了不通的 JSON 数据场景,就不过多介绍了。

使用了 Gson 之后,遇到花括号 {}会生成一个 JsonObject,而字符串则是基本类型的 JsonPrimitive 对象,它们在 Gson 内部的解析流程是不一样的,这就造成了 IllegalStateException 异常。

那么接下来看看如何解决这个问题。

既然 TypeAdapter 是 Gson 解析的银弹,找不到解决方案,用它就对了。思路继续是用 JsonDeserializer 来接管解析,这一次将 User 类的整个解析都接管了。

class UserGsonAdapter:JsonDeserializer{    override fun deserialize(json: JsonElement,                              typeOfT: Type?,                              context: JsonDeserializationContext?): User {                var user = User()        if(json.isJsonObject){            val jsonObject = JSONObject(json.asJsonObject.toString())            user.name = jsonObject.optString("name")            user.age = jsonObject.optInt("age")            user.languageStr = jsonObject.optString("languages")            user.languages = ArrayList()            val languageJsonArray = JSONArray(user.languageStr)            for(i in 0 until languageJsonArray.length()){                user.languages.add(languageJsonArray.optString(i))            }        }        return user    }}fun userGsonStr(){    val jsonStr = """        {            "name":"承香墨影",            "age":"18",            "languages":["CN","EN"]        }    """.trimIndent()    val user = GsonBuilder()            .registerTypeAdapter(                    User::class.java,                    UserGsonAdapter())            .create()            .fromJson(jsonStr,User::class.java)    Log.i("cxmydev","user: \n${user.toString()}")}

在这里我直接使用标准 API org.json 包中的类去解析 JSON 数据,当然你也可以通过 Gson 本身提供的一些方法去解析,这里只是提供一个思路而已。

最终 Log 输出的效果如下:

{"name":"承香墨影","age":18,"languagesJson":["CN","EN"],"languages size:"2}

在这个例子中,最终解析还是使用了标准的 JSONObject 和 JSONArray 类,和 Gson 没有任何关系,Gson 只是起到了一个桥接的作用,好像这个例子也没什么实际用处。

不谈场景说应用都是耍流氓,那么如果是使用 Retrofit 呢?Retrofit 可以配置 Gson 做为数据的转换器,在其内部就完成了反序列化的过程。这种情况,配合 Gson 的 TypeAdapter,就不需要我们在额外的编写解析的代码了,网络请求走一套逻辑即可。

如果觉得在构造 Retrofit 的时候,为 Gson 添加 TypeAdapter 有些入侵严重了,可以配合 @JsonAdapter注解使用。

三. 小结时刻

针对服务端返回数据的容错处理,很大一部分其实都是来自双端没有保证数据一致的问题。而针对开发者来说,要做到外部数据均不可信的,客户端不信本地读取的数据、不信服务端返回的数据,服务端也不能相信客户端传递的数据。这就是所谓防御式编程。

言归正传,我们小结一下本文的内容:

TypeAdapter(包含JsonSerializer、JsonDeserializer) 是 Gson 解析的银弹,所有 Json 解析的定制化要求都可以通过它来实现。registerTypeAdapter()方法需要制定确定的数据类型,如果想支持继承,需要使用 registerTypeHierarchyAdapter()方法。如果数据量不大,推荐使用 JsonSerializer 和 JsonDeserializer。针对整个 Java Bean 的解析接管,可以使用 @JsonAdapter注解。

[Gson]Gson 解析 Json 容错策略-前沿热点

2023-03-31

焦点信息:四川广元聚力打造川陕甘结合部区域医疗中心和医养基地

2023-03-31

实力不错的2所河南院校,招生专业好,招生分数不高,考生可了解

2023-03-30

谢谢你,杨洪涛医生!_世界新视野

2023-03-30

全球百事通!江西剑指涉企违规收费 促进企业减负降费

2023-03-30

唐山一天游必去的景点_唐山周边自驾游景点

2023-03-30

我和我的雄安丨扎根_当前焦点

2023-03-30

中健网农财务造假案论文_中健网

2023-03-30

徐汇区积极推进河道水生态健康评估工作

2023-03-30

中国最大铅锌矿启动开发,新疆如何建成世界级铅锌产业基地?-环球今日讯_热消息

2023-03-30

环球百事通!聚焦“分享课堂”,高埂学校教师到芳草小学“取经”

2023-03-30

当前热门:《非人先生的新娘》,甜到不行的爱恋,超萌的卡内诺基,好喜欢

2023-03-30

2023年中国香薰行业供需分析及发展前景

2023-03-30

纯碱的用途怎么应用在工业上?纯碱在生活里主要用途有哪些?|当前快报

2023-03-30

有时禁用或拒绝接受cookie会导致此问题 全球快播报

2023-03-30

当前通讯!绩效考核评价表模板(绩效考评表模板)

2023-03-30

唐人神:公司在智能养猪领域做了一些试点-天天时讯

2023-03-30

东宏股份:目前公司向特定对象发行A股股票项目正在积极推进中 天天热文

2023-03-30

当前快看:老人 82 元只买到两个素菜?校方回应_全球视点

2023-03-30

上实发展(600748):3月29日北向资金减持34.03万股|每日报道

2023-03-30

【全球报资讯】小米百合桂圆粥的功效,小米百合粥的禁忌(润肠通便小米百合桂圆粥含有丰富的膳食纤维)

2023-03-30

博融智库拟从广州市博逸珩仲投资管理处以0元受让其持有的博融产业的52%股权 出资额为260万-今日要闻

2023-03-29

《阳神之太上忘情》上映,落魄书生逆天改命,修仙入道战妖魔|当前速递

2023-03-29

生态环境部坚持以“零容忍”态度依法查处环境违法行为 看热讯

2023-03-29

收购硅谷银行未必“名利双收” 美国Holding 家族能否延续百年基业长青|世界报道

2023-03-29

孙国友水源被断后续:女儿哭诉保不住一根水管,资本的力量太强大 世界报资讯

2023-03-29

85后厨师的斜杠人生:苦练10多年捏“活”面塑-天天时快讯

2023-03-29

GSMA:中国5G连接2025年将超10亿 2030年将达16亿

2023-03-29

全球视点!外交部:中方坚决反对任何形式的美台官方往来

2023-03-29

岷县市场:当归整体交易稳定 成交价格坚挺 环球观焦点

2023-03-29

ETF追踪:昨日ETF净申购10.6亿元 资金加仓中证1000ETF指数

2023-03-29

黑色期货高开震荡,钢价涨幅有限-天天报道

2023-03-29

倒计时15天!2023春季第37届大河国际车展重磅来袭!-世界看热讯

2023-03-29

点映票房破2亿的《八佰》到底能不能救市?看体量也要看质量|环球微动态

2023-03-29

【世界时快讯】海口30日举办大中城市联合招聘高校毕业生专场

2023-03-29

【世界报资讯】梅西少C罗20球!但大赛淘汰赛10-3完胜 强队克星VS虐菜狂魔

2023-03-29

净利大增446%!比亚迪年报岀炉,单车净利也上涨,出海成重要方向

2023-03-29

全球快讯:天际汽车,缺钱续命

2023-03-29

环球速讯:快讯2023-03-29 01:42:34

2023-03-29

积分兑换可免费乘高铁 官方:10000积分起兑

2023-03-28

天天速讯:“韦神”出题,初二学生给出标准答案!

2023-03-28

新能源和医疗要反转了?_精彩看点

2023-03-28

全球速递!阿里巴巴将重组为六个主要业务部门 基本情况讲解

2023-03-28

张家口天宏医院王慧东怎么样?天宏和维多利亚哪个好?一起PK!|世界新动态

2023-03-28

Meta(META.US)拟降低部分员工奖金 以继续推进增效计划|全球今亮点

2023-03-28

世界百事通!自贡荣县新时代家校社协同育人指导中心(站)授牌成立

2023-03-28

杰瑞股份:油气市场对设备和服务需求活跃 2022年归母净利润22.57亿元 同比涨42.31% 天天简讯

2023-03-28

韩乔生:扬科维奇率领国足有进步,接下来重点是阵容大换血

2023-03-28

信用贷不还钱会怎么样?连累家人吗?

2023-03-28

广州生育津贴最迟什么时候向单位申请?

2023-03-28

【发现春之美】极目定山海 蓦首古城春 全球讯息

2023-03-28

这次,港资房企赢得彻底

2023-03-28

婴儿吃完奶一直打嗝是怎么回事_婴儿老打嗝怎么办

2023-03-28

明天过后歌词谁写的_明天过后 歌词 全球微头条

2023-03-28

*ST炼石(000697):第三次临时股东大会增加临时提案

2023-03-28

大葱炒鸡蛋怎么做好吃_大葱炒鸡蛋的烹饪方法

2023-03-27

年纪相加九十九,却尬玩“爷孙恋”“奶孙恋”,他们老糊涂了吗?|环球快播

2023-03-27

前白蛋白低是什么意思_前白蛋白

2023-03-27

全球速讯:教育部部署开展“全国中小学生安全教育周”活动

2023-03-27

博主:今天传出留洋的球员不是戴伟浚,和他有接触的不是法国球队

2023-03-27

资讯:深度品尝了青岛啤酒、金徽酒和舍得酒的复星,更偏好哪个口味?

2023-03-27

旗舰同款!真我GT Neo5 SE搭载柔性直屏:1.5K 144Hz高刷 聚看点

2023-03-27

天禾股份:公司目前暂未与俄罗斯钾肥有直接的化工产品贸易往来 当前热文

2023-03-27

港股异动 | 昊海生物科技(06826)升5% 22年归母净利降约49% 末期息每股0.4元_焦点速读

2023-03-27

视焦点讯!宝马i Vision Dee将于4月17日中国首发

2023-03-27

强降雨袭击巴西致数千人无家可归

2023-03-27

CJ:目前锡安的精神状态非常好 当球队人员齐整时我们会很棒

2023-03-27

最新快讯!长沙市开福区赴深圳招才招“财”

2023-03-27

绍兴新昌开展“世界水日”“中国水周”系列宣传活动

2023-03-26

焦点热文:菂读啥音_菂

2023-03-26

力有余却在前期没打好 环球热闻

2023-03-26

【全球播资讯】4月:82年生肖狗切勿不管不顾,70年,94年生肖狗也应理智为宜!

2023-03-26

圣枪哥是哪个战队的

2023-03-26

英语动词有哪些变形规则(英语动词有哪些)

2023-03-26

若可控核聚变成为现实,世界将会变成什么样子?科学家给出答案 热点评

2023-03-26

英汉-汉英翻译教程-学生用书_关于英汉-汉英翻译教程-学生用书的简介 环球快消息

2023-03-26

焦点日报:内娱红毯向奥斯卡看齐!童瑶穿高定一脸狠劲,女王气场逆袭章子怡

2023-03-26

小年吃什么传统食物 小年习俗有哪些_今日聚焦

2023-03-25

肇庆某商圈交通秩序问题,相关部门终于开始整治了…|世界新视野

2023-03-25

快播:痛风病的症状有哪些_痛风病症状有哪些

2023-03-25

北人生而不识菱者文言文翻译阅读_北人生而不识菱者文言文翻译 全球微头条

2023-03-25

世界观点:赤水河的发源地

2023-03-25

天天最资讯丨上位法优于下位法原则的顺序_上位法优于下位法原则

2023-03-25

20分钟寻回!“岳西警察叔叔太棒了!”

2023-03-25

屡次发生?地摊玩偶小熊里竟藏微型摄像头!

2023-03-25

求都市炼器小说 天天微资讯

2023-03-25

新时代新征程新伟业·两会精神进基层丨让古丈毛尖香飘万里 快看

2023-03-25

车市价格战引发行业洗牌 业内人士称价值战才是正解

2023-03-25

nba职业生涯总得分榜_nba职业生涯总得分排名 天天快讯

2023-03-25

产品提价难抵成本上涨 金龙鱼2022年净利跌超两成 未来对产品盈利能力改善有信心_世界观天下

2023-03-24

天天时讯:探索古驰全新Horsebit 1955手袋广告形象大片主角的个性风格

2023-03-24

韩文静是什么电视剧 快消息

2023-03-24

“ChatGPT之父”推虹膜扫描计划验证身份 未来可区分人与AI

2023-03-24

当前速读:《完美国际》残云三章(暗潮大帐(95级以上区域))

2023-03-24

人民银行开展规章、规范性文件清理工作 废止11件规章和36件规范性文件_环球观速讯

2023-03-24

手沾到502胶水怎么快速解决_520胶水粘到手怎么办

2023-03-24

焦点速读:涨停雷达:ST板块异动 *ST中昌触及涨停

2023-03-24

美!开往春天的列车带你穿越花海

2023-03-24

每日视讯:什么是计算机控制系统

2023-03-24

焦点热议:十大最难懂方言(十大最难听的歌)

2023-03-24