一. 序章
文章评论里后台有一些小伙伴,针对具体数据容错的场景,提出了具体的问题。今天就在这篇文章里统一解答,并且给出解决方案。
【资料图】
二. 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
注解。 标签:
-
[Gson]Gson 解析 Json 容错策略
文章评论里后台有一些小伙伴,针对具体数据容错的场景,提出了具体的问题。今天就在这篇文章里统一解答,并且给出解决方案。
-
静音轮胎排名_静音轮胎
1、静音轮胎: 汽车在行驶当中由轮胎本身所产生的噪音较小,一定程度上提升了车主驾驶时的“舒适性”。2、 (一)设计原
-
公司推荐函的范文(汇总6篇) 全球热推荐
公司推荐函的范文第1篇尊敬的领导:您好!感谢您阅读我的升职自我推荐信,为我打开了一扇希望之窗。在此,希望您能让我毛遂自荐,晋升我为部门
-
前方高能预警_前方高能是什么意思-世界最新
1、一般认为最早出自《机动战士高达》系列,在各ACG弹幕网的视频弹幕中经常会出现“前方高能”之类的弹幕,预示接下来视频一
-
山西新绛:万亩桃花灼灼开 古城激活“桃经济”
新绛县万安仙果品专业合作社作为当地油桃种植和销售的龙头合作社,带动民众增收致富。”新绛县万安仙果品专业合作社理事长崔振虎说,主打的...
-
环球快看:1-2月国有企业利润总额同比增长9.9%
中新网3月30日电题:1-2月国有企业利润总额同比增长9 9%中新财经记者赵建华财政部30日公布,1-2月,全国国有
-
超图软件: 独立董事关于续聘会计师事务所的事前认可意见-天天微动态
超图软件:独立董事关于续聘会计师事务所的事前认可意见
-
全球球精选!关于天气的谚语(关于天气的谚语二年级上册)
1、早霞不出门,晚霞行千里。2、早立秋凉飕飕,晚立秋热到头。3、早看东南,晚看西北。4、早晨喜鹊叫,天气定晴好。5、云自东北起,必定有风雨
-
我们的家园|以茶富民 在创新中激活映秀镇文旅国际范
15年眨眼而过,今天一起来看看汶川县映秀镇发生了哪些新变化。这里不仅记录着“5 12”大地震的重要历史,还致力于建设全国重要的爱国主义...
-
每日讯息!螃蟹秋天肥的原因?
说秋天是吃蟹的季节,这是因为季节与蟹的生长质量有很大的关系,秋季是螃蟹生长最好的时期,它们的肉厚肥嫩,且味美色香,为一年当中最鲜美。
-
30页论文,俞士纶团队新作:AIGC全面调查,从GAN到ChatGPT发展史 每日热讯
奇点已来?
-
2023智慧商旅行业发展前景及投资风险
未来智慧商旅行业市场机会在哪?当前,随着人们出游观念的转变,越来越多的人提倡“可持续发展的智慧旅游”,而企业应当通过更积极地用户互...
-
环球快资讯丨港股异动 | 微创机器人-B(02252)跌5% 22年归母亏损扩大95.5%
微创机器人-B(02252)绩后小幅走低,午后跌约5%。22年,公司实现收入同比增长904 79%,归母亏损同比扩大95 53%。期末,现金及现金等价物由约19
-
黑暗武士刷图加点_黑暗武士刷图加点
Dnf黑暗骑士加点主要是选择大技能,另外就是魔流输出比较高。刷图主要是因为魔流的技能比较好,所以选择魔流的技能点要满。无
-
洛阳选派63名国企干部到深圳开展跟班学习
记者从29日召开的选派国企干部到深圳跟班学习动员会上获悉,洛阳选派63名国企干部,将于下周到深圳开展为期3个月的跟班学习。选派优秀年轻干部
-
当前消息!截至发稿,花房集团(03611.HK)跌9.05%、百度集团-SW(09888.HK)跌4.45%、阅文集团(00772.HK)跌4.42%、知乎-W(02390.HK)跌3.98%
截至发稿,花房集团(03611 HK)跌9 05%、百度集团-SW(09888 HK)跌4 45%、阅文集团(00772 HK)跌4 42%、知乎-W(02390 HK)跌3 98%
-
Canalys:2022 年第四季度全球网络安全的规模达到196亿美元 渠道出货量表现强劲
3月30日,Canalys发文称,2022年第四季度,全球网络安全技术的总支出仍然保持强劲,而由渠道主导的强劲出货,弥补了直销出货的下降。市场规模
-
全国用水总量去年控制在6100亿立方米内
本报北京3月29日电(记者王浩)记者从近日在京召开的节约用水工作部际协调机制2023年度全体会议上获悉,2022年,全国用水总量控制在6100亿立方
-
柳林:多部门联合开展“治超月”集中宣传活动-世界短讯
柳林:多部门联合开展“治超月”集中宣传活动
-
环球快资讯丨网络视听超即时通讯成第一大网络应用 用户规模达10.40亿
网络视听超即时通讯成第一大网络应用用户规模达10 40亿
-
什么都不要对我说铃声_什么都不要对我说
1、歌曲名:什么也不要对我说演唱:浩瀚☆鉁稀↘爱★制跟我在一起你就是哪飘飞的泪水花晴朗而明亮而去又无瞰你就是
-
鱼肚的泡发方法视频_鱼肚的泡发 今热点
1、鱼肚在食用前,必须提前泡发,其方法有油发和水法两种。2、质厚的鱼肚两种发法皆可,而质薄的鱼肚,水发易烂,还是采用油发
-
为什么甜玉米不建议吃? 今头条
1 甜味玉米有毒在外面购买玉米往往有甜和不甜两种,而我们自制的玉米往往没有这些甜味。玉米中含有淀粉,只有在咀嚼时和口腔中的淀粉酶混合材会
-
已有人遇难,中国驻泰国使领馆,紧急提醒-当前独家
近期,个别赴泰旅游中国同胞在参与海岛浮潜项目时不幸遇难。浮潜活动看似难度较低,实际具有较高风险性,每年在泰因浮潜遇难事故频繁发生,中
-
极致狂暴 抢先试驾特斯拉Model X Plaid
说实话这么多年看了无数场电动车的发布会,在说到对标车型时,特斯拉依旧是被提及最多的名字,尽管它的风评两极分化严重,但其标杆的地位至今
-
世界热点!肩周炎是哪个部位疼痛_肩周炎是哪个部位
1、肩周炎典型特点:肩关节周围疼痛,肩关节活动受限。2、看看肩关节有没有活动受限,活动时疼不疼。3、往上举,往外展,手往
-
当前聚焦:中国移动月租8元套餐好用吗_中国移动月租8元套餐
1、越来越多的用户选择了“8元号码保护套餐”。2、很多手机号用久了,更换起来很麻烦,所以我们干脆用8元的套餐来更换,保留
-
极无双2平民玩家培养哪三个?
极无双2这款游戏以三国为背景,展开一场有血有肉的战争。游戏还是很不错的,无论是游戏的操作界面的精细度,还是技能释放的流畅度。
-
热文:蓝光电影资源网_bd电影资源网
1、BD是BlueDisk的简称,翻译成中文是“蓝光影碟”的意思。2、DVD的激光头现在用的是橙红色,兰光的波长更小,
-
世界快讯:硬核科技论【Vol.28】宝马推出全景HUD,别出心裁还是多此一举?
只有一点我持保留意见,就是我觉得目前这个全景HUD,全景这俩字儿贯彻的还不够彻底,影像投射的面积确实不大,当然这套HUD离正式投产还有点时