Jackson之多态反序列化
1.场景描述
JSON作为一种轻量级的数据交换格式,其清晰和简洁的结构能够轻松地与Java对象产生映射关系。例如,一个Coke(可口可乐)类的java代码如下:
public class Coke{
String name = "Coke";
int capacity= 500;
}
用json描述该类:
{
"name":"Coke",
"capacity":500
}
而这种映射关系可以通过代码进行转换,也就是所谓的json序列化和反序列化。
序列化:是指将Java对象转换成Json文件或者Json字符串;
反序列化:是指将Json文件或者Json字符串转换成Java对象。
Java代码实现Json的序列化和反序列化并不难,尤其是现在的很多框架简化了很多的过程。下面以我常用的jackson
为例,实现简单的json序列化和反序列化:
Coke类的定义如下
public class Coke {
public String name;
public int capacity;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
}
下面是测试类:
public class JsonTest {
@Test
public void JsonTest() throws IOException {
ObjectMapper mapper = new ObjectMapper();
String jsonStr = " {\n" +
" \"name\":\"Coke\",\n" +
" \"capacity\":500\n" +
"}";
//json deserialization
Coke coke = mapper.readValue(jsonStr,Coke.class);
System.out.println(coke.capacity);
//json serialization
Coke coke1 = new Coke();
coke1.setName("BigCoke");
coke1.setCapacity(680);
String serializationJson = mapper.writeValueAsString(coke1);
System.out.println(serializationJson);
}
}
输出结果:
对单个类的序列化和反序列化,只要不是结构过于复杂,其操作还是比较简单的。对于此类型的序列化和反序列在这里我就不赘述了。
我们现在要讨论的情况是,假如我们现在要对json对象进行反序列化操作,但是我们并不知知道具体的json格式,也就是说我们不知道json有哪些字段。这在实际生活中很常见,比如在商店中,每件商品都有不同的属性。饮料会有容量属性,而马桶,我们一般不会去考虑"容量"这种东西吧。那我们又该如何去做这种可能性很多的反序列化呢?
问题:我们可以做反序列化,但是我们得知道这个json文件或者字符串对应的类,而上述的情况没法做到"运行前"就知道是什么商品。只有在用户付款时(运行时),我们才知道这个商品是什么。
分析:我们没法在运行前知道需要反序列化的
商品
是什么,但是我们知道一共有哪些商品
可以被反序列化。而反序列化所需要的类我们也可以在工程中根据商品类型直接定义。我们要做的只是在获取到商品
时告诉它需要反序列化成哪个对象就OK了。而商品类型,我们可以根据商品名来判断。那我们现在需要的就是一种可以根据json文件或json字符串中某个字段判断出需要反序列化成哪一种对象的方法。幸运的是,jackson也提供了解决这类问题的方案。
2. 多态类型的处理
Jackson支持多态类型配置,在进行jackson反序列化时,可以根据配置转换成相应的子类对象。
其配置主要时通过相关的注解实现的。
@JsonTypeInfo
查看注解定义,其结构如图:
由上图可以看出,这个注解一共有4个字段,分别是
use
,include
,property
和defaultImpl
。下面分别对这4个字段进行说明。
-
Id
类型的use
这个字段时用来指定根据哪种类型的元数据来进行序列化和反序列化。可选的值有:- JsonTypeInfo.Id.CLASS
- sonTypeInfo.Id.MINIMAL_CLASS
- JsonTypeInfo.Id.NAME
- JsonTypeInfo.Id.CUSTOM
- JsonTypeInfo.Id.NONE
这里我们选择的是JsonTypeInfo.Id.NAME
这个值,它表示的是我们的Serde将会使用字段名称作为依据。针对上述场景,我们将会根据商品名称来进行serde。
-
As
类型的include
这个字段是用来指定我们的元信息是如何被包含进去的,可选的值如下:- JsonTypeInfo.As.PROPERTY
- JsonTypeInfo.As.EXISTING_PROPERTY
- JsonTypeInfo.As.EXTERNAL_PROPERTY
- JsonTypeInfo.As.WRAPPER_OBJECT
- JsonTypeInfo.As.WRAPPER_ARRAY
这个字段我们选择的是JsonTypeInfo.As.PROPERTY,它所表示的意思是包含机制将会使用一个具体的属性值。
-
String
类型的property
只用当use
为JsonTypeInfo.Id.CUSTOM
,或者include
为JsonTypeInfo.As.PROPERTY
时才会配置这个值。这个就可以用来表示具体的依据字段。
下面是该注解的使用 :
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY,property = "productName")
这个表示的就是在进行反序列化时,我们依据productName
这个字段来区分需要转换成的对象。例如,productName="Coke"
时,我们就将json反序列化成Coke对象。
@JsonSubTypes
这个注解是结合上个注解一起完成多态反序列化的。上个注解指定了反序列化的标标识,而这个注解指定了每个标识对应的子类。
注解的结构如下:
由注解的结构图可以看出,这注解只有一个字段就是
@Type
类型的数组。而@Type的value就是子类,name即为子类对应的标识。下面是该注解的使用:
@JsonSubTypes(value = {
@JsonSubTypes.Type(value = ClassA.class, name = "A"),
@JsonSubTypes.Type(value = ClassA.class, name = "B")
})
上面代码所做的工作就是,当检测标识为“A”时就将其反序列化ClassA,为“B”时就反序列化成ClassB。
既然已经知道两个注解的用法了,接下来我们就通过一个Demo看看他们在我们的代码中该如何发挥作用。
3. Demo
场景描述:近日,某游戏厂家出品一种新的游戏装备实体卡。玩家购买实体卡通过扫码之后就可以获得相应的道具,这些卡机具收藏价值。而每张卡的道具都是通过json来描述的,当玩家扫描后,后台就会根据这些描述信息把装备卡转换成相应的道具。目前已出的装备卡有三种,星空魔杖
,代达罗斯之殇
和巨大瓶饮料
。三个装备的描述信息分别如下:
- 星空魔杖
{
"name":"Star wand" ,
"length":35,
"price":120,
"effect":["getting greater", "getting handsome","getting rich"]
}
- 代达罗斯之殇
{
"name":"Daedalus",
"weight":"5kg",
"damage":1200,
"roles":["assassinator","soldier"],
"origin":{
"name":"Mainland of warcraft",
"date":"142-12-25"
}
}
- 巨大瓶饮料
{
"name":"Huge drink",
"capacity":500000,
"effect":"quenching your thirst and tasting good"
}
首先定义父类,用于反序列化时指定参数。
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,property = "name")
@JsonSubTypes(value = {
@JsonSubTypes.Type(value = Daedalus.class, name = "Daedalus"),
@JsonSubTypes.Type(value = HugeDrink.class, name = "Huge drink"),
@JsonSubTypes.Type(value = StarWand.class, name = "Star wand"),
})
public interface Equipment {
}
这个接口的定义很简单,只是为了将各种装备划分成一类。然后通过注解指定了其子类类型,子类的标识字段以及每个子类对应的标识值。
然后根据描述信息我们可以很轻松地写出这三个类的定义:
public class StarWand implements Equipment{
private String name;
private int length;
private int price;
private List<String> effect;
public void setName(String name) {
this.name = name;
}
public void setLength(int length) {
this.length = length;
}
public void setPrice(int price) {
this.price = price;
}
public void setEffect(List<String> effect) {
this.effect = effect;
}
public String getName() {
return name;
}
public int getLength() {
return length;
}
public int getPrice() {
return price;
}
public List<String> getEffect() {
return effect;
}
}
public class Daedalus implements Equipment {
private String name;
private String weight;
private int damage;
private List<String> roles;
private Map<String,String> origin;
public void setName(String name) {
this.name = name;
}
public void setWeight(String weight) {
this.weight = weight;
}
public void setDamage(int damage) {
this.damage = damage;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public void setOrigin(Map<String, String> origin) {
this.origin = origin;
}
public String getName() {
return name;
}
public String getWeight() {
return weight;
}
public int getDamage() {
return damage;
}
public List<String> getRoles() {
return roles;
}
public Map<String, String> getOrigin() {
return origin;
}
}
public class HugeDrink implements Equipment{
private String name;
private int capacity;
private String effect;
public void setName(String name) {
this.name = name;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public void setEffect(String effect) {
this.effect = effect;
}
public String getName() {
return name;
}
public int getCapacity() {
return capacity;
}
public String getEffect() {
return effect;
}
}
最后是主方法
public class Main {
public static void main(String[] args) throws IOException {
String starWandStr = "{\n" +
" \"name\":\"Star wand\" ,\n" +
" \"length\":35,\n" +
" \"price\":120,\n" +
" \"effect\":[\"getting greater\", \"getting handsome\",\"getting rich\"]\n" +
"}";
String daedalusStr = "{\n" +
" \"name\":\"Daedalus\",\n" +
" \"weight\":\"5kg\",\n" +
" \"damage\":1200,\n" +
" \"roles\":[\"assassinator\",\"soldier\"],\n" +
" \"origin\":{\n" +
" \"name\":\"Mainland of warcraft\",\n" +
" \"date\":\"142-12-25\"\n" +
" }\n" +
"}";
String hugeDrinkStr = "{\n" +
" \"name\":\"Huge drink\",\n" +
" \"capacity\":500000,\n" +
" \"effect\":\"quenching your thirst and tasting good\"\n" +
"}";
ObjectMapper mapper = new ObjectMapper();
StarWand starWand = (StarWand)mapper.readValue(starWandStr, Equipment.class);
Daedalus daedalus = (Daedalus)mapper.readValue(daedalusStr, Equipment.class);
HugeDrink hugeDrink = (HugeDrink)mapper.readValue(hugeDrinkStr, Equipment.class);
System.out.println("大佬!您已获得星空魔杖!属性增幅:"+ starWand.getEffect().toString()+"!");
System.out.println("大佬!您已获得代达罗斯之殇,增加了 " + daedalus.getDamage() + " 点输出!");
System.out.println("大佬!您已获得代达巨大瓶饮料,it "+ hugeDrink.getEffect()+"!");
}
}
控制台输出结果如下:
后记
首先需要注意的是,在做json反序列化时,javaBean可以定义getter方法,但是setter方法必须定义。
再有就是当我们有多个子类的时候,在基类上的注解就会显的很长。我们也有其他的方式可以实现。ObjectMapper类提供了一个registerSubtypes
,通过这个方法我们可以直接注册子类,就是说我们不需要在定义基类的时候使用JsonSubTypes
这个注解了。
mapper.registerSubtypes(new NamedType(HugeDrink.class, "Huge drink"));
mapper.registerSubtypes(new NamedType(Daedalus.class, "Daedalus"));
mapper.registerSubtypes(new NamedType(StarWand.class, "Star wand"));
上面的这中写法可以达到与JsonSubTypes
注解相同的效果。
Demo地址:https://github.com/BigRantLing/JsonSerde