[ES三周年]Gson 解析 Json 容错策略
一. 序章
文章评论里后台有一些小伙伴,针对具体数据容错的场景,提出了具体的问题。今天就在这篇文章里统一解答,并且给出解决方案。
(资料图片)
二. 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 类中包含 name
和 age
两个字段,其中 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
注解。
标签:
推荐文章
- [ES三周年]Gson 解析 Json 容错策略
- 信宇人将于3月30日上会:计划募资4.6亿元,比亚迪为主要客户_环球热资讯
- 全国首部!《南京市地下电动汽车库防火设计导则》6月1日实施|天天快资讯
- 英恒科技(01760.HK):2022年归母净利大增107%至4.15亿元 末期息13.1港仙_世界微头条
- 天天即时:能不能抓一波新能源的反弹?
- 世界报道:我市2022年“急救大数据”出炉 创伤 心脑血管疾病 呼吸系统疾病排紧急呼救疾病前三位
- 每日播报!如何编中国结_最简单的编法介绍
- 警惕!这家合资车企再陷“退市”传闻!官方否认背后:月销仅330辆,负债65.74亿元
- 招商银行王良:壮大财富管理客群规模、提升服务能力和水平
- 《小丑2》新路透图像:Lady Gaga扮小丑女强吻老年女性抗议者 当前动态
- 卓创资讯:关于人工智能方面的问题,请您查阅3月24日披露的年报交流会披露文件 世界快资讯
- 总投资15亿元!宁夏伟力全钒液流电池储能项目预计6月投产 天天观天下
- 跨境电商“日子没以前滋润了”,逆风下如何保持盈利 环球新资讯
- 北京市4月20日起实施口腔种植医疗服务收费、种植体集中带量采购、牙冠限价挂网综合治理 15项种植牙相关医疗服务均价将降72%
- 地球上线唐陌傅闻夺肉在第几章(地球上线唐陌傅闻夺肉)-天天热资讯
- 上海虹桥机场恢复国际航线-每日热门
- 雅域光源有限公司(雅域)
- 整合数百战机,北欧四国打造“联合空军”-每日动态
- 任何数的零次方等于多少_观热点
- 基金:这只基金应该这么玩 今日快讯
- 环球时讯:市场调研的方法及优缺点怎么写_市场调研的方法及优缺点
- 增值税发票几个点?
- 七十二层奇楼赵丽颖第几期出现 环球新要闻
- 【伊朗外交部:美国在叙非法驻军违反国际法】当地时间25日,伊朗外交部发言人卡纳尼表示,美国在叙利亚部分地区的非法驻军和占领,以及其对叙境内的各种目标进行袭击等行为违反了国际法,侵犯了叙利亚的国家主权和领土完整。(央视新闻)
- 清明前后,多吃“1花1芽2野菜”!养肝祛湿,采摘短,眼下正当吃
- 河北邢台市临城县3.3级地震-环球快报
- 百色民族高中宿舍图片_百色民族高中 焦点快播
- 每日速讯:蝠鲼怎么读
- 【世界新视野】好123 设为主页_好123com设置主页
- 北京城市副中心开启四级联动公共就业服务模式,招聘会开到商业综合体
- 热门看点:踏青赏花季 黄兴公园海棠文化节迎来一片粉色花海
- 北京锦绣大地批发市场地址|环球观速讯
- 业绩增速超行业累计均值3倍,探寻药明生物(02269)强劲基本面背后的成长确定性 今日要闻
- 当前信息:美国参议员称索尼垄断日本高端游戏市场 要求发起调查
- 城投控股股东户数减少169户,户均持股13.4万元_热讯
- 国家税务局发票真伪查询官网(国家税务局发票真伪查询)
- 闭上的反义词
- 200位老师和学生同台PK餐厅服务调酒赛技能|焦点要闻
- 【环球时快讯】远光软件亮相2023中国医院智慧财务高峰论坛 分享医院智慧运营建设实践
- 南京磐能电力科技股份有限公司(期货套利)
- 机器蜂箱可在寒冷中为蜂群提供重要的生命支持并引导其行为
- 广日股份(600894)3月24日主力资金净卖出173.06万元-世界微头条
- 平板电脑能连接液晶电视吗
- 天天简讯:代理记账的收费标准
- 州绿色钛产业建设工作专班到云南钛业调研 每日资讯
- 歌词想把你抱在身体里面_想把你抱在身体里面 世界新资讯
- 环球快报:湖北省黄石市人大常委会党组副书记、副主任王新华接受纪律审查和监察调查
- 多部门联合组织开展农村能源革命试点县建设
- 苏州固锝03月23日获深股通增持199.35万股
- eve是什么意思-天天快资讯
- 怎样把手机视频转换成mp4格式文件_怎样把手机视频转换成MP4格式|世界时快讯
- 昭衍新药实控人半个月套现2.65亿元,此前已连续两个季度减持
- 政策直通车!群众关心的60个常见问题→_世界热点评
- 沈福村:深入农户开展反电诈、反邪教宣传活动_世界今日讯
X 关闭
最新资讯
- 全球今亮点!海南省海口市发布雷雨大风黄色预警
- 高考特长有哪些
- Chuwi GemiBook xPro:采用英特尔 Alder Lake-N 处理器的新型低功耗笔记本电脑发布 关注
- 遭遇首次年度营收下滑,安踏“现金奶牛”FILA不香了?|全球动态
- 今日热闻!昆明至曼谷、万象国际航线将加密
- 张德芹亲自下基层打鸡血,习酒单飞只是看上去很美
- 环球时讯:爱丽丝梦游仙境症_关于爱丽丝梦游仙境症介绍
- 欧美银行股暴跌加剧市场动荡 黄金走势继续下行
- 2022年新年祝福语四字成语
- 曝新西兰VS国足最低票价不足54元 体坛预测国足首发武磊谭龙搭档锋线林良铭刘彬彬两翼
- komho是什么轮胎
- 3月22日基金净值:太平丰润一年定开债发起式最新净值0.9539,跌0.01%-全球关注
- 产能过剩是什么意思通俗_产能过剩是什么意思
- 今年三伏天是几月几号到几月几号 三伏天时间表2021 滚动
- 追梦力挺嘴哥:换位思考下 如果你离开工作几天就被造谣是啥感觉
- 环球微速讯:宝清县气象台发布大风蓝色预警【IV级/一般】
- 资讯推荐:霍华德超人再现梦回巅峰,身穿24号致敬科比,如何看待霍华德今天的扣篮?
- 固始县南大桥乡:提高政务服务效率,筑牢贴心服务理念
- 卡通女孩头像_卡通女孩简笔画 天天滚动
- 凰仪煤矿瓦斯事故调查报告公布:煤矿被罚316万 多名责任人存在对抗调查情况
- 贵州超级算力,让观众提前十年看上《流浪地球2》
- 买入开仓和卖出开仓是什么意思_买入开仓和卖出开仓意思简述
- 三医院医院看男科多少钱
- 精选!周敦颐爱莲说带拼音_周敦颐爱莲说拼音版
- 【速看料】w7激活工具 w7
- 天天热点评!耕地保护、临河道路加装安全护栏、监管农家乐……他们这样服务保障乡村振兴
- 帕森斯:里夫斯应拿更大合同 感兴趣的球队应该我的合同去刺激他
- 月子护理机构“美丽承诺”转眼成空
- 天天快消息!增值税视同销售的情形_列举增值税视同销售的五种情形
- 国家地表水优良水质断面比例达87.9%(新数据 新看点) 全球最新
- 证监会:加大发行上市全链条监管力度 对重大违法违规行为“零容忍”_微速讯
- 快资讯丨德邦证券吴开达:A股行业配置“大科技”为抓手,风格看好中证1000
- 女网友约见面的骗局_我与60多岁女网友 聚焦
- 【天天热闻】自建迁移EMR实践案例
- 短讯!凯莱英:黄小莲辞任高级副总裁
- 姜虎东李昇基将合作新综艺 延续《强心脏》概念 热点聚焦
- 科普书籍读后感怎么写_科普书籍读后感
- 职业健康安全管理体系包括哪些内容_职业健康_天天热讯
- 工商银行龙凤呈祥金条100克价格今天多少一克(2023年03月20日)
- 艾米莉·狄金森:最终,劳作者与嬉游者将同归于无形
- 全球快播:国羽夺2冠2亚!陈雨菲1-2惜败,00后黑马爆冷夺冠,石宇奇0-2惨败
- 多巴胺是什么意思有什么作用_dba是什么意思|今日播报
- 每日看点!装了二套房,总结出客厅“8要8不要”!很实用的建议,值得收藏
- 中国女篮最新消息!李梦正式回应,新领队走马上任,李月汝有收获
- 一般胆管癌都能活多久会传染_一般胆管癌都能活多久
- 最新快讯!监狱怎么办假释?
- 井底之蛙告诉我们什么道理简短_井底之蛙告诉我们什么道理
- 微软在其Surface Hardware活动中展示了许多新设备_聚看点
- 职业规划怎么写_规划怎么写
- 岗位晋级申请书怎么写_晋级申请书怎么写-环球头条
X 关闭