如何更好的使用 Gson

用户头像
Jackey
关注
发布于: 2020 年 06 月 14 日
如何更好的使用Gson

最近工作比较忙,很久没更新了,先向大家道个歉。

今天想分享一些工作中遇到的关于 Gson 的坑,这么说其实不太准确,因为不能算是 Gson 的坑,更多的是因为旧代码产生了一些不规范的数据导致使用 Gson 时遇到了一些问题。

Gson 简介

可能有的同学不了解 Gson ,所以在分享坑之前先来介绍一下 Gson ,已经熟练使用 Gson 的同学可以直接跳到下一部分了。Gson 是Google开源的一个Java序列化库,它具有以下特点:

  • 使用简单,只需要掌握toJson()fromJson()两个方法就可以实现 Java 对象和 JSON 字符串之间的序列化和反序列化

  • 允许将现有的不可修改的对象与 JSON 互相转换

  • 对 Java 的泛型支持的很好

  • 允许自定义一些对象的表现形式

  • 支持复杂对象的序列化

使用 Gson

那现在我们就来体验一下 Gson 的第一个特性,使用简单。由团队中成员的能力参差不齐,所以一个简单易用性对这种基础组件是非常重要的。

在使用 Gson 之前,我们需要添加依赖,我们的项目中使用的是 Maven 管理依赖,所以会在 pom.xml 文件中插入以下代码:

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>



如果你的项目使用的是 Gradle 管理依赖,你需要新增下面的代码

dependencies {
implementation 'com.google.code.gson:gson:2.8.6'
}



依赖添加好以后,就可以直接开始使用了,这里我先来定义一个简单的 POJO 类(原谅我直接使用@Data)。

import lombok.Data;
@Data
public class User {
private String name;
private Integer age;
private String email;
private Boolean isVip;
}



来看一下输出结果

结果



完美!是不是非常简单?

那现在我们已经学会 Gson 的基础用法了,接下来就进入正题,分享几个我在使用过程中遇到的实际问题以及解决方案。

案例分享

null 转为空字符串

在我们的使用过程中,遇到过这样的情况对于一个对象,在做序列化的时候,如果遇到了某个 item 为 null ,那么 Gson 序列化出来的结果中就不会包含这个属性,这看起来很合理,不过对于我们的项目而言,前端同学需要根据有没有这个 item 来展示不同的信息,如果有这个 item ,但是值为空,那么前端就展示「不能告诉你」,如果没有这个 item ,前端同学就会展示为「没有这个 item」。这么说可能令人有些费解,我们还是通过上面的例子来看。

{
"age":18,
"email":"Jackeyzhe59@gmail.com",
"isVip":true
}



如果我把 name 设置为 null ,那么序列化的结果就是这样。此时前端就会展示为「用户没有姓名信息」,如果我把 name 设置成空字符串,那么序列化结果就会不同。

{
"name":"",
"age":18,
"email":"Jackeyzhe59@gmail.com",
"isVip":true
}



这时我们的前端同学就会告诉用户,此用户不想展示名称。

我们现在想要避免出现第一种情况,虽然说可以约定不能把name设置为null,但是这种约定就很容易导致 bug 的产生,尤其是对于刚刚加入团队的新同学来说,他们可能会在不经意间就做了这样一个操作,在code review 的时候也可能会出现遗漏。因此我选择定义一种TypeAdapter来约束我们序列化的工作。

这里可以先介绍一下 Gson 中 TypeAdapter 的使用方法,TypeAdapter 可以帮助我们自定义序列化/反序列化方式,它的使用也比较简单,首先我们需要定义一个自己的 Adapter 类,让它继承 TypeAdapter 类

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class StringNullAdapter extends TypeAdapter<String> {
@Override
public void write(JsonWriter out, String value) throws IOException {
if (value == null) { // 序列化使用的是adapter的write方法
out.value("");
return;
}
out.value(value);
}
@Override
public String read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) { // 反序列化使用的是read方法
in.nextNull();
return "";
}
return in.nextString();
}
}



然后自己重写 read 和 write 方法,这里我们需要的是 write 方法。

其中参数 value 就是传入的对象属性,我们判断它是 null,就将其转化为空字符串。

写好 Adapter 类之后,我们在新建 Gson 的时候需要注册我们刚刚定义的 Adapter。

Gson gson = new GsonBuilder()
.registerTypeAdapter(String.class, new StringNullAdapter())
.create();



GsonBuilder 提供了 registerTypeAdapter 这个方法,可以直接为 String 类型都注册上我们自己的Adapter。

这时再将 name 设置为 null,序列化结果就是我们期望的结果了。

数字和 Boolean 到底用哪个

我们在开发过程中还遇到了这样一个问题,在和另一个 node 写的服务做交互时,我们发现,node 服务返回给我们的 JSON 对应的 Boolean 类型字段的值是0或1。

还用我们前面的例子来讲,如果 node 服务返回给我们的数据是这样的 JSON 字符串

{
"name":"Jackey",
"age":18,
"email":"Jackeyzhe59@gmail.com",
"isVip":1
}



那么我们在反序列化时就会报错

报错信息



错误信息写的很清楚,我们的 isVip 字段是一个 Boolean 类型的,但是 JSON 中却是数字类型,Gson 没办法识别了。

这时我们可以让 node 服务来修改,也可以选择自己做适配。

自己做适配的话,有两种方式,一种是把 isVip 字段改成 Number 类型,但是由于 isVip 只可能存在两种值(是/否),用 Number 类型不是很合适。另一种方式就是再写一个 Adapter 来做适配,这次我们就需要重写 read 方法了。

public Boolean read(JsonReader in) throws IOException {
JsonToken peek = in.peek();
switch (peek) {
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
case NUMBER:
return in.nextInt() != 0;
case STRING:
return Boolean.parseBoolean(in.nextString());
default:
throw new IllegalStateException("Expected BOOLEAN or NUMBER but was " + peek);
}
}



针对我们的问题,只需要处理 NUMBER 类型就可以了,不过这里我还兼容了 STRING 类型,把字符串的 true/false 转换成 Boolean 类型。你可以根据业务需要自行适配。例如有些团队可能会将 null 值认为是 false,这里直接修改一下就好。

写好了 Adapter 以后还是别忘了注册。

扩展一点

细心的同学一定注意到了 JsonToken 这个类了,这是 Gson 中对于 JSON 符号类型的定义。它包含以下几种

  • BEGIN_ARRAY

  • END_ARRAY

  • BEGIN_OBJECT

  • END_OBJECT

  • NAME

  • STRING

  • NUMBER

  • BOOLEAN

  • NULL

  • END_DOCUMENT

从名称上就可以分辨出来BEGIN_ARRAYEND_ARRAY是对数组的标记,BEGIN_OBJECTEND_OBJECT是对对象的标记,NAME标记的是 JSON 中的「key」,STRINGNUMBERBOOLEANNULL都是 JSON中值的类型,END_DOCUMENT是 JSON 流结束的标识。

讨论

最后留一个问题大家可以和我一起讨论,我们在做反序列化时还遇到了BT的字符串的 null,它本身所属的字段是 Map 类型,这样的 Adapter 应该怎么写呢?



发布于: 2020 年 06 月 14 日 阅读数: 34
用户头像

Jackey

关注

什么都知道一点的程序员 2018.03.09 加入

还未添加个人简介

评论

发布
暂无评论
如何更好的使用Gson