~ read.

Json容错总结

一、 序

技术简历的技能树这一项中,JSONGSON都是常客。但是还有面试候选者将他们的理解停留在最简单的使用上。
"JSON是一种具有自描述的、独立于语言的、轻量级文本数据交换格式,经常被用于数据的存储和传输。而 GSON 可以帮我们快速的将 JSON 数据,在对象之间序列化和反序列化。"
GSON 的 toJson() 和 fromJson() 这两个方法,是 GSON 最基本的使用方式,它很直观,也没什么好说的。但当被问及 GSON 如何对 JSON 数据容错,如何灵活序列化和反序列化的时候,就有点抓瞎了。
JSON 数据容错,最简单的方式是让前后端数据保持一致,就根本不存在容错的问题,但是现实场景中,并不如我们预期的那般美好。
举两个简单的例子:User 类中的姓名,有些接口返回的 Key 值是 name,而有些返回的是 username,如何做容错呢?再比如 age 字段返回的是如 "18" 这样的字符串,而 Java 对象将其解析成 Int 类型时,虽然 GSON 有一定的类型容错性,这样解析能够成功,但是如果 age 字段的返回值变成了 "" 呢,如何让其不抛出异常,并且设置为默认值 0?
在本文中,我们就来详细看看,GSON 是如何对数据进行容错解析的。

二、GSON 的容错

  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 就只涉及到这两个方法,但是针对一些特殊的场景,就没那么简单了。

2.GSON 的注解
GSON 提供了注解的方式,来配置最简单的灵活性,这里介绍两个注解 @SerializedName 和 @Expose。
@SerializedName 可以用来配置 JSON 字段的名字,最常见的场景来自不同语言的命名方式不统一,有些使用下划线分割,有些使用驼峰命名法。
还是拿 User 类来举例,Java 对象中定义的用户名称,使用的是 userName,而返回的 JSON 数据中,用的是 user_name,这样的差异就可以用 @SerializedName 来解决。

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

而在前文中,针对同一个 User 对象中的用户名称,现在不同的接口返回有差异,分别为:name、user_name、username,这种差异也可以用 @SerializedName 来解决。
在 @SerializedName 中,还有一个 alternate 字段,可以对同一个字段配置多个解析名称。

class User{
    @SerializedName(value = "user_name",alternate = arrayOf("name","username"))
    var userName :String? = null
    var gender = 0
    var 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"))
    @Expose
    var userName :String? = null
    @Expose
    var gender = 0
    var age = 0

    @Expose(serialize = true,deserialize = false)
    var genderDesc = ""
}

fun User.gsonTest(){
    // 序列化
    val user = User()
    user.userName = "Android开发架构"
    user.age = 18
    user.gender = 1
    user.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 解析数据的问题,更灵活的使用方式,就不是注解可以解决的了。

3.GsonBuilder 灵活解析
就像前面的例子中看到的一样,想要构造一个 Gson 对象,有两种方式,new Gson() 和利用 GsonBuilder 构造。
这两种方式都可以构造一个 Gson 对象,但是在这个 Builder 对象,还提供一些快捷的方法,方便我们更灵活的解析 JSON。
例如默认情况下,GSON 是不会解析为 null 的字段的,而我们可以通过.serializeNulls() 方法,来让 GSON 序列化为 null 的字段。

// 序列化
val user = User()
user.age = 18
user.gender = 1

val 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 还提供了更多的操作:

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

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

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

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

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

.setPrettyPrinting():格式化输出

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

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 = 1

val 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<Int> {
    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 数据返回的格式,和后端开发“沟通”一下午规范的问题。
坚持规范当然没有错,但是因为别人的问题导致自己的工作无法继续,就不符合精益思维了。

  • GSON 可以提供了 toJson() 和 fromJson() 两个简便的方法序列化和反序列化 JSON 数据。
  • 通过注解@SerializedName可以解决解析字段不一致的问题以及多字段的问题。
  • 通过注解@Expose可以解决字段在序列化和反序列化时,字段排除的问题。
  • GsonBuilder 提供了一些便捷的 API,方便我们解析数据。
  • 更灵活的解析,使用TypeAdapter,可以精准定制序列化和反序列化的全过程。