热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

接口返回的JSON,再离谱也有办法,谈谈JSON容错!

一、序技术简历的技能树这一项中,JSON和GSON都是常客。但是还有面试候选者将他们的理解停留在最简单的使用上。“JSON是一种具有自描述的、独立于语言的、轻量级

一、序

技术简历的技能树这一项中,JSON 和 GSON 都是常客。但是还有面试候选者将他们的理解停留在最简单的使用上。

“JSON 是一种具有自描述的、独立于语言的、轻量级文本数据交换格式,经常被用于数据的存储和传输。而 GSON 可以帮我们快速的将 JSON 数据,在对象之间序列化和反序列化。”

GSON 的 toJson() 和 fromJson() 这两个方法,是 GSON 最基本的使用方式,它很直观,也没什么好说的。但当被问及 GSON 如何对 JSON 数据容错,如何灵活序列化和反序列化的时候,就有点抓瞎了。

JSON 数据容错,最简单的方式是让前后端数据保持一致,就根本不存在容错的问题,但是现实场景中,并不如我们预期的那般美好。

举两个简单的例子:User 类中的姓名,有些接口返回的 Key 值是 name,而有些返回的是 username,如何做容错呢?再比如 age 字段返回的是如 "18" 这样的字符串,而 Java 对象将其解析成 Int 类型时,虽然 GSON 有一定的类型容错性,这样解析能够成功,但是如果 age 字段的返回值变成了 "" 呢,如何让其不抛出异常,并且设置为默认值 0?

在本文中,我们就来详细看看,GSON 是如何对数据进行容错解析的。

二、GSON 的容错


2.1 GSON 的常规使用

GSON 是 Google 官方出的一个 JSON 解析库,比较常规的使用方式就是用toJson() 将 Java 对象序列化成 JSON 数据,或者用  fromJson() 将 JSON 数据反序列化成 Java 对象。

// 序列化
val user = User()
user.userName = "Android开发架构"
user.age = 18
user.gender = 1
val jsonStr = Gson().toJson(user)
Log.i("cxmydev","json:$jsonStr")
// json:{"age":18,"gender":1,"userName":"Android开发架构"}// 反序列化
val newUser = Gson().fromJson(jsonStr,User::class.java)
Log.i("cxmydev","userName:${newUser.userName}")
// userName:Android开发架构

GSON 很方便,大部分时候并不需要我们额外处理什么,拿来即用。唯一需要注意的可能就是泛型擦除,针对泛型的解析,无非就是参数的差异而已。

在数据都很规范的情况下,使用 GSON 就只涉及到这两个方法,但是针对一些特殊的场景,就没那么简单了。

免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加入【安卓开发架构】

2.2 GSON 的注解

GSON 提供了注解的方式,来配置最简单的灵活性,这里介绍两个注解 @SerializedName 和 @Expose。

@SerializedName 可以用来配置 JSON 字段的名字,最常见的场景来自不同语言的命名方式不统一,有些使用下划线分割,有些使用驼峰命名法。

还是拿 User 类来举例,Java 对象中定义的用户名称,使用的是 userName,而返回的 JSON 数据中,用的是 user_name,这样的差异就可以用 @SerializedName 来解决。

class User{@SerializedName("user_name")var userName :String? = nullvar gender = 0var age = 0
}

而在前文中,针对同一个 User 对象中的用户名称,现在不同的接口返回有差异,分别为:nameuser_nameusername,这种差异也可以用 @SerializedName 来解决。

在 @SerializedName 中,还有一个 alternate 字段,可以对同一个字段配置多个解析名称。

class User{@SerializedName(value = "user_name",alternate = arrayOf("name","username"))var userName :String? = nullvar gender = 0var age = 0
}

再来看看 @Expose,它是用来配置一些例外的字段。

在序列化和反序列化的过程中,总有一些字段是和本地业务相关的,并不需要从 JSON 中序列化出来,也不需要在传递 JSON 数据的时候,将其序列化。

这样的情况用 @Expose 就很好解决。从字面上理解,将这个字段暴露出去,就是参与序列化和反序列化。而一旦使用 @Expose,那么常规的 new Gson() 的方式已经不适用了,需要 GsonBuilder 配合.excludeFieldsWithoutExposeAnnotation() 方法使用。

@Expose 有两个配置项,分别是 serialize 和 deserialize,他们用于指定序列化和反序列化是否包含此字段,默认值均为 True。

class User{@SerializedName(value = "user_name",alternate = arrayOf("name","username"))@Exposevar userName :String? = null@Exposevar gender = 0var age = 0@Expose(serialize = true,deserialize = false)var genderDesc = ""
}fun User.gsonTest(){// 序列化val user = User()user.userName = "Android开发架构"user.age = 18user.gender = 1user.genderDesc = "男"val gson = GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()val jsonStr = gson.toJson(user)Log.i("cxmydev","json:$jsonStr")// json:{"gender":1,"genderDesc":"男","user_name":"承香墨影"}val newUser = gson.fromJson(jsonStr,User::class.java)Log.i("cxmydev","genderDesc:${newUser.genderDesc}")// genderDesc:
}

可以看到上面的例子中,genderDesc 用于说明性别的描述,使用@Expose(serialize = true,deserialize = false) 标记后,这个字段只参与序列化(serialize = true),而不参与反序列化(deserialize = false)。

需要注意的是,一旦开始使用 @Expose 后,所有的字段都需要显式的标记是否“暴露”出来。上例中,age 字段没有 @Expose 注解,所以它在序列化和反序列化的时候,均不会存在。

GSON 中的两个重要的字段注解,就介绍完了,正确的使用他们可以解决 80% 关于 GSON 解析数据的问题,更灵活的使用方式,就不是注解可以解决的了。

2.3 GsonBuilder 灵活解析

就像前面的例子中看到的一样,想要构造一个 Gson 对象,有两种方式,new Gson() 和利用 GsonBuilder 构造。

这两种方式都可以构造一个 Gson 对象,但是在这个 Builder 对象,还提供一些快捷的方法,方便我们更灵活的解析 JSON。

例如默认情况下,GSON 是不会解析为 null 的字段的,而我们可以通过.serializeNulls() 方法,来让 GSON 序列化为 null 的字段。

// 序列化
val user = User()
user.age = 18
user.gender = 1val jsonStr = GsonBuilder().create().toJson(user)
Log.i("cxmydev","json:$jsonStr")
// json:{"age":18,"gender":1}val jsonStr1 = GsonBuilder().serializeNulls().create().toJson(user)
Log.i("cxmydev","json1:$jsonStr1")
// json1:{"age":18,"gender":1,"userName":null}

GsonBuilder 还提供了更多的操作:

  1. .serializeNulls() :序列化为 null 的字段。

  2. .setDateFormat():设置日期格式,例如:setDateFormat("yyyy-MM-dd")

  3. .disableInnerClassSerialization():禁止序列化内部类。

  4. .generateNonExcutableJson():生成不可直接解析的 JSON,会多 )]}' 这 4 个字符。

  5. .disableHtmlEscaping():禁止转移 HTML 标签

  6. .setPrettyPrinting():格式化输出

无论是注解还是 GsonBuilder 中提供的一些方法,都是 GSON 针对一些特殊场景下,为我们提供的便捷  API,更复杂一些的场景,就不是它们所能解决的了。

2.4 TypeAdapter

如果前面介绍的规则,都满足不了业务了,没关系,Gson 还有大招,就是使用 TypeAdapter。

这里讲的 TypeAdapter 是一个泛指,它虽然确实是一个 GSON 库中的抽象类,但在 GSON 的使用中,它又不是一个类。

使用 TypeAdapter 就需要用到 GsonBuilder 类中的 registerTypeAdapter(),我们先来看看这个类的方法实现。

public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) {$Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer|| typeAdapter instanceof JsonDeserializer|| typeAdapter instanceof InstanceCreator|| typeAdapter instanceof TypeAdapter);if (typeAdapter instanceof InstanceCreator) {instanceCreators.put(type, (InstanceCreator) typeAdapter);}if (typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer) {TypeToken typeToken = TypeToken.get(type);factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter));}if (typeAdapter instanceof TypeAdapter) {factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter));}return this;}

可以看到注册方法,需要制定一个数据类型,并且它除了支持 TypeAdapter 之外,还支持 JsonSerializer 和 JsonDeserializer。InstanceCreator 的使用场景太少了,就不谈了。

TypeAdapter(抽象类)、JsonSerializer(接口)、JsonDeserializer(接口) 都可以理解成我们前面说的 TypeAdapter 的泛指,他们具体有什么区别呢?

TypeAdapter 中包含两个主要的方法 write() 和 read() 方法,分别用于接管序列化和反序列化。而有时候,我们并不需要处理这两种情况,例如我们只关心 JSON 是如何反序列化成对象的,那就只需要实现 JsonDeserializer 接口的 deserialize() 方法,反之则实现 JsonSerializer 接口的 serialize() 方法,这让我们的接管更灵活、更可控。

需要注意的是,TypeAdapter 之所以称之为大招,是因为它会导致前面介绍的所有配置都失效。但并不是使用了 TypeAdapter 之后,所有的规则都需要我们自己实现。注意看 registerTypeAdapter() 方法的第一个参数是指定了类型的,它只会针对某个具体的类型进行接管。

举个例子就清楚了,例如前文中提到,当一个 "" 的 JSON 字段,碰上一个 Int 类型的字段时,就会导致解析失败,并抛出异常。

// 序列化
val user = User()
user.age = 18
user.gender = 1val jsonStr = "{\"gender\":\"\",\"user_name\":\"Android开发架构\"}"val newUser = GsonBuilder().create().fromJson(jsonStr,User::class.java)
Log.i("cxmydev","gender:${gender}")

在上面的例子中,gender 字段应该是一个 Int 值,而 JSON 字符串中的gender 为 "",这样的代码,跑起来会抛JsonSyntaxException: java.lang.NumberFormatException: empty String 异常。

我们实现 JsonDeserializer 接口,来接管反序列化的操作。

class IntegerDefault0Adapter : JsonDeserializer {override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Int {try {return json!!.getAsInt()} catch (e: NumberFormatException) {return 0}}
}

当转 Int 出现异常时,返回默认值 0。然后使用 registerTypeAdapter() 方法加入其中。

val newUser = GsonBuilder().registerTypeAdapter(Int::class.java, IntegerDefault0Adapter()).create().fromJson(jsonStr,User::class.java)
Log.i("cxmydev","gender : ${newUser.gender}")
// gender : 0

TypeAdapter 的使用,到这里就介绍完了,这个大招只要放出来,所有 JSON 解析的问题都不再是问题。TypeAdapter 的适用场景还很多,可以根据具体的需求具体实现,这里就不再过多介绍了。

另外再补充几个 TypeAdapter 的细节。

1. registerTypeHierarchyAdapter() 的区别

看看源码,细心的朋友应该发现了,注册 TypeAdapter 的时候,还有registerTypeHierarchyAdapter() 方法,它和 registerTypeAdapter() 方法有什么区别呢?

区别就在于,接管的类型类,是否支持继承。例如前面例子中,我们只接管了 Int 类型,而数字类型还有其他的例如 Long、Float、Double 等并不会命中到。那假如我们注册的是这些数字类型的父类 Number 呢?使用 registerTypeAdapter() 也不会被命中,因为类型不匹配。

此时就可以使用 registerTypeHierarchyAdapter() 方法来注册,它是支持继承的。

2. TypeAdapterFactory 工厂类的使用

使用 registerXxx() 方法可以链式调用,注册各种 Adapter。

如果嫌麻烦,还可以使用 TypeAdapterFacetory 这个 Adapter 工厂,配合registerTypeAdapterFactory() 方法,根据类型来返回不同的 Adapter。

其实只是换个了实现方式,并没有什么太大的区别。

3. @JsonAdapter 注解

@JsonAdapter 和前面介绍的 @SerializedName、@Expose 不同,不是作用在字段上,而是作用在 Java 类上的。

它指定一个“Adapter” 类,可以是 TypeAdapter、JsonSerializer 和 JsonDeserializer 这三个中的一个。

@JsonAdapter 注解只是一个更灵活的配置方式而已,了解一下即可。

三、小结时刻

GSON 很好用,但是也是建立在使用正确的基础上。我见识过一些丑陋的代码,例如多字段场景下,也在 Java 对象中配套写上多个字段,再增加一个方法用于返回多个字段中不会 null 的字段。又或者为了一个 JSON 数据返回的格式,和后端开发“沟通”一下午规范的问题。

坚持规范当然没有错,但是因为别人的问题导致自己的工作无法继续,就不符合精益思维了。

不抽象,就无法深入思考,我们还是就今天的内容做一个简单的小结。

  1. GSON 可以提供了 toJson() 和 fromJson() 两个简便的方法序列化和反序列化 JSON 数据。

  2. 通过注解 @SerializedName 可以解决解析字段不一致的问题以及多字段的问题。

  3. 通过注解 @Expose 可以解决字段在序列化和反序列化时,字段排除的问题。

  4. GsonBuilder 提供了一些便捷的 API,方便我们解析数据,例如

  5. 更灵活的解析,使用 TypeAdapter,可以精准定制序列化和反序列化的全过程。

就总结五条吧,多了也记不住。

免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加入【安卓开发架构】

推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 解决VS写C#项目导入MySQL数据源报错“You have a usable connection already”问题的正确方法
    本文介绍了在VS写C#项目导入MySQL数据源时出现报错“You have a usable connection already”的问题,并给出了正确的解决方法。详细描述了问题的出现情况和报错信息,并提供了解决该问题的步骤和注意事项。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • React基础篇一 - JSX语法扩展与使用
    本文介绍了React基础篇一中的JSX语法扩展与使用。JSX是一种JavaScript的语法扩展,用于描述React中的用户界面。文章详细介绍了在JSX中使用表达式的方法,并给出了一个示例代码。最后,提到了JSX在编译后会被转化为普通的JavaScript对象。 ... [详细]
author-avatar
孤鹰与战狼
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有