如何更好的使用Gson
点击上方“蓝字”关注我们吧!
最近工作比较忙,很久没更新了,先向大家道个歉。
今天想分享一些工作中遇到的关于gson的坑,这么说其实不太准确,因为不能算是gson的坑,更多的是因为旧代码产生了一些不规范的数据导致使用gson时遇到了一些问题。
gson简介
可能有的同学不了解gson,所以在分享坑之前先来介绍一下gson,已经熟练使用gson的同学可以直接跳到下一部分了。gson是Google开源的一个Java序列化库,它具有以下特点:
-
使用简单,只需要掌握
toJson()
和fromJson()
两个方法就可以实现Java对象和JSON字符串之间的序列化和反序列化 -
允许将现有的不可修改的对象与JSON互相转换
-
对Java的泛型支持的很好
-
允许自定义一些对象的表现形式
-
支持复杂对象的序列化
使用gson
那现在我们就来体验一下gson的第一特性,使用简单。由团队中成员的能力参差不齐,所以一个简单易用性对这种基础组件是非常重要的。
在使用gson之前,我们需要添加依赖,我们的项目中使用的是Maven管理依赖,所以会在pom.xml文件中插入以下代码:
com.google.code.gson gson 2.8.6
如果你的项目使用的是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了,直接看一个demo吧。
public class GsonTest { public static void main(String[] args) { User user = new User(); user.setName("Jackey"); user.setAge(18); user.setEmail("Jackeyzhe59@gmail.com"); user.setIsVip(true); Gson gson = new Gson(); String json = gson.toJson(user); System.out.println(json); User u = gson.fromJson(json, User.class); System.out.println(u.getName()); } }
来看一下输出结果
完美!是不是非常简单?
那现在我们已经学会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 { @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_ARRAY
和 END_ARRAY
是对数组的标记, BEGIN_OBJECT
和 END_OBJECT
是对对象的标记, NAME
标记的是json中的「key」, STRING
、 NUMBER
、 BOOLEAN
和 NULL
都是json中值的类型, END_DOCUMENT
是json流结束的标识。
讨论
最后留一个问题大家可以和我一起讨论,我们在做反序列化时还遇到了BT的字符串的null,它本身所属的字段是Map类型,这样的Adapter应该怎么写呢?