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);
    }
}

输出结果:

输出结果.png

对单个类的序列化和反序列化,只要不是结构过于复杂,其操作还是比较简单的。对于此类型的序列化和反序列在这里我就不赘述了。
我们现在要讨论的情况是,假如我们现在要对json对象进行反序列化操作,但是我们并不知知道具体的json格式,也就是说我们不知道json有哪些字段。这在实际生活中很常见,比如在商店中,每件商品都有不同的属性。饮料会有容量属性,而马桶,我们一般不会去考虑"容量"这种东西吧。那我们又该如何去做这种可能性很多的反序列化呢?
问题:我们可以做反序列化,但是我们得知道这个json文件或者字符串对应的类,而上述的情况没法做到"运行前"就知道是什么商品。只有在用户付款时(运行时),我们才知道这个商品是什么。
分析:我们没法在运行前知道需要反序列化的商品是什么,但是我们知道一共有哪些商品可以被反序列化。而反序列化所需要的类我们也可以在工程中根据商品类型直接定义。我们要做的只是在获取到商品时告诉它需要反序列化成哪个对象就OK了。而商品类型,我们可以根据商品名来判断。那我们现在需要的就是一种可以根据json文件或json字符串中某个字段判断出需要反序列化成哪一种对象的方法。幸运的是,jackson也提供了解决这类问题的方案。

2. 多态类型的处理

Jackson支持多态类型配置,在进行jackson反序列化时,可以根据配置转换成相应的子类对象。
其配置主要时通过相关的注解实现的。
@JsonTypeInfo
查看注解定义,其结构如图:

@JsonTypeInfo.png

由上图可以看出,这个注解一共有4个字段,分别是use,include,propertydefaultImpl。下面分别对这4个字段进行说明。

  • Id类型的use
    这个字段时用来指定根据哪种类型的元数据来进行序列化和反序列化。可选的值有:

    1. JsonTypeInfo.Id.CLASS
    2. sonTypeInfo.Id.MINIMAL_CLASS
    3. JsonTypeInfo.Id.NAME
    4. JsonTypeInfo.Id.CUSTOM
    5. JsonTypeInfo.Id.NONE
      这里我们选择的是JsonTypeInfo.Id.NAME这个值,它表示的是我们的Serde将会使用字段名称作为依据。针对上述场景,我们将会根据商品名称来进行serde。
  • As类型的include
    这个字段是用来指定我们的元信息是如何被包含进去的,可选的值如下:

    1. JsonTypeInfo.As.PROPERTY
    2. JsonTypeInfo.As.EXISTING_PROPERTY
    3. JsonTypeInfo.As.EXTERNAL_PROPERTY
    4. JsonTypeInfo.As.WRAPPER_OBJECT
    5. JsonTypeInfo.As.WRAPPER_ARRAY
      这个字段我们选择的是JsonTypeInfo.As.PROPERTY,它所表示的意思是包含机制将会使用一个具体的属性值。
  • String类型的property
    只用当useJsonTypeInfo.Id.CUSTOM,或者includeJsonTypeInfo.As.PROPERTY时才会配置这个值。这个就可以用来表示具体的依据字段。
    下面是该注解的使用 :

@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY,property = "productName")

这个表示的就是在进行反序列化时,我们依据productName这个字段来区分需要转换成的对象。例如,productName="Coke"时,我们就将json反序列化成Coke对象。

@JsonSubTypes
这个注解是结合上个注解一起完成多态反序列化的。上个注解指定了反序列化的标标识,而这个注解指定了每个标识对应的子类。
注解的结构如下:

@JsonSubTypes.png

由注解的结构图可以看出,这注解只有一个字段就是@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()+"!");
    }
}

控制台输出结果如下:

image.png

后记

首先需要注意的是,在做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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容