Gson 优雅实现多个枚举的自定义(反)序列化过程

版本说明

  JDK版本

JDK版本说明

  Gson版本

1
2
3
4
5
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>

Gson (反)序列化例子

  Gson是实现对象序列化和反序列化的利器,但是Gson在(反)序列化枚举时默认是根据枚举值的名称来实现的,如果你想要在(反)序列化枚举时输出自己定义的枚举属性,那么此时至少有两种选择:

  • 继承TypeAdapter抽象类并实现其中的抽象方法
  • 实现JsonSerializer 和/或JsonDeserializer 接口

  我通常是选择第二种方式,即实现接口的方式来自定义(反)序列化过程。比如下面这个实例:

  针对JDK1.8新的时间API中的LocalDate进行自定义(反)序列化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.gson.*;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class LocalDateTypeAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) {
return null == src ? null : new JsonPrimitive(formatter.format(src));
}
@Override
public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return null == json ? null : LocalDate.parse(json.getAsString(), formatter);
}
}

  当注册了该TypeAdapterGson在序列化LocalDate类型时,会调用其中的serialize方法,此时如果LocalDate实例非空的话,我们将调用formatter.format(src)方法将LocalDate实例格式化成指定形式的字符串,然后封装成JsonPrimitive实例并返回GsonGson再将返回的JsonPrimitive写入到 json 字符串中。

  同理,当Gson反序列化时遇到LocalDate类型时,会调用其中的deserialize方法完成反序列化。

枚举(反)序列化通用实现

  好了,扯完基本(反)序列化实现后,现在有一个问题:那就是针对单个枚举使用这种方式那是无可厚非的。但是当你的项目中应用到的枚举数量比较多的时候,如果依然采用这种方式,那么随着而来的问题是类的增加以及出现一些小范围的重复代码。

  要如何解决这个问题就是写作本文的意图了。

  本文是基于JsonSerializerJsonDeserializer 接口来解决问题的,当然网上有针对TypeAdapter的工厂类TypeAdapterFactory来解决该问题,但是该解决方案有个不足之处,那就是需要修改Gson的源码,这里就不讲该方案了,有需要的同学自行搜索。

  第一步:一个接口,该接口定义了Gson在(反)序列化枚举时将调用的方法:

1
2
3
4
5
6
7
public interface GsonEnum<E> {
String serialize();
E deserialize(String jsonEnum);
}

  接口中定义接收一个泛型E,该泛型用来表示枚举类型,里面有两个抽象方法,String serialize()方法表示将枚举序列化成字符串,E deserialize(String jsonEnum)表示将字符串反序列化成枚举并返回特定的枚举E

  第二步:定义枚举类并实现第一步中声明的接口,这里为了演示效果,定义了如下两个枚举:

  派别枚举Faction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public enum Faction implements GsonEnum<Faction> {
ABNEGATION("无私派"), AMITY("和平派"), DAUNTLESS("无畏派"), CANDOR("诚实派"), ERUDITE("博学派");
private final String name;
Faction(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Faction parse(String faction) {
switch (faction) {
case "无私派":
return Faction.ABNEGATION;
case "和平派":
return Faction.AMITY;
case "无畏派":
return Faction.DAUNTLESS;
case "诚实派":
return Faction.CANDOR;
case "博学派":
return Faction.ERUDITE;
default:
throw new IllegalArgumentException("There is not enum names with [" + faction + "] of type Faction exists! ");
}
}
@Override
public Faction deserialize(String jsonEnum) {
return Faction.parse(jsonEnum);
}
@Override
public String serialize() {
return this.getName();
}
}

  性别枚举Gender

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public enum Gender implements GsonEnum<Gender> {
MALE("男"), FEMALE("女");
private final String type;
Gender(String type) {
this.type = type;
}
public String getType() {
return type;
}
public static Gender parse(String type) {
switch (type) {
case "男":
return Gender.MALE;
case "女":
return Gender.FEMALE;
default:
throw new IllegalArgumentException("There is not enum names with [" + type + "] of type Gender exists! ");
}
}
@Override
public Gender deserialize(String jsonEnum) {
return Gender.parse(jsonEnum);
}
@Override
public String serialize() {
return this.getType();
}
}

  对于这两个枚举内部对GsonEnum接口的实现方法比较简单,所以这里就不解释了。

  第三步:自定义Gson(反)序列化过程的GsonEnumTypeAdapter,先看具体代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class GsonEnumTypeAdapter<E> implements JsonSerializer<E>, JsonDeserializer<E> {
private final GsonEnum<E> gsonEnum;
public GsonEnumTypeAdapter(GsonEnum<E> gsonEnum) {
this.gsonEnum = gsonEnum;
}
@Override
public JsonElement serialize(E src, Type typeOfSrc, JsonSerializationContext context) {
if (null != src && src instanceof GsonEnum) {
return new JsonPrimitive(((GsonEnum) src).serialize());
}
return null;
}
@Override
public E deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (null != json) {
return gsonEnum.deserialize(json.getAsString());
}
return null;
}
}

  代码看起来很简单,唯一的构造器要求传入一个GsonEnum的实现类。

  首先看一个测试用例:

  (反)序列化测试时用到的类Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person implements Serializable {
private String name;
private Gender gender;
private Faction faction;
private LocalDate birthday;
public Person() {
}
public Person(String name, Gender gender, Faction faction, LocalDate birthday) {
this.name = name;
this.gender = gender;
this.faction = faction;
this.birthday = birthday;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", gender=" + gender +
", faction=" + faction +
", birthday=" + birthday +
'}';
}
// 省略 getter 和 setter
}

实现效果测试

  JUnit测试实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testGson() throws Exception {
Gson gson = new GsonBuilder()
.serializeNulls()
.registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter())
.registerTypeAdapter(Gender.class, new GsonEnumTypeAdapter<>(Gender.FEMALE))
.registerTypeAdapter(Faction.class, new GsonEnumTypeAdapter<>(Faction.ABNEGATION))
.create();
Person p1 = new Person("雷卡", Gender.MALE, Faction.DAUNTLESS, LocalDate.of(1994, 10, 11));
System.out.println("调用 toString 方法:\n" + p1);
String jsonText = gson.toJson(p1);
System.out.println("将 person 转换成 json 字符串:\n" + jsonText);
System.out.println("-------------------");
Person p2 = gson.fromJson(jsonText, Person.class);
assert p2 != p1;
System.out.println("根据 json 字符串生成 person 实例:\n" + p2);
}

  测试结果为:

测试结果

实现原理说明

  测试结果表明我们想要的效果已经达到了,现在讲一下GsonEnumTypeAdapter的实现原理。

  以Faction为例,首先它接收E时我们传递的是Faction枚举的一个枚举值Faction.ABNEGATION,这个枚举值没有任何限定,可以是该枚举类中的任意一个。

  序列化Faction类型时,Gson调用其中的serialize方法,注意此时我们传入的Faction.ABNEGATION并不发挥作用,我们直接调用的是序列化时遇到的Faction实例(比如Faction.AMITYFaction.CANDOR)的serialize实例方法,那么此时返回的必然是该枚举实例的name属性值;

  反序列化遇到Faction类型时,Gson调用其中的deserialize方法,此时我们实际上操作的最开始传入的枚举类型Faction.ABNEGATIONdeserialize方法,注意此时该方法需要传入一个字符串,刚好可以用到JsonElement自身提供的getAsString方法。再看Faction类中对deserialize方法的实现,实际上该方法并不关注调用者是谁,它真正关心的传入值是什么,根据传入的值,它可以很快获取得到对应的枚举或者抛出异常。

  可能上面表述得不是很清楚,但实际上都是一些基础的知识,即泛型,接口,枚举的特殊性的综合应用,多看几遍应该就懂了,另外最好打上几个断点,然后debug一遍,看整个流程是怎么跑的,那样基本就清晰了。

  另外,为确保GsonEnumTypeAdapter只接收枚举并且GsonEnum只被枚举类所实现,可以添加泛型参数的边界,如下:

  GsonEnum的类声明

1
public interface GsonEnum<E extends Enum<E>>

  GsonEnumTypeAdapter的类声明

1
public class GsonEnumTypeAdapter<E extends Enum<E>> implements JsonSerializer<E>, JsonDeserializer<E>

  声明了泛型上限为Enum之后可以确保在使用该泛型时只有枚举类能够被处理。

源码地址:GitHub

编写日期:2016-08-23
发布日期:2017-05-22