在Springboot + Mybaitis-plus 项目中利用Jackson实现json对java多态的(反)序列化
阅读原文时间:2021年07月23日阅读:1

Jackson允许配置多态类型处理,当JSON面对的转换对象是一个接口、抽象类或者一个基类的时候,可以通过一定配置实现JSON的转换。在实际项目中,Controller层接收入参以及在Dao层将对象以json的形式存入数据库时都可能会遇到这个问题。而Springboot和mp都支持使用Jackson处理json,从而可以利用Jackson的特点,解决这一问题。

注意

为了代码简洁,这里的代码忽略了set和get方法和构造函数

在本例中,父类Zoo有两个子类Dog和Cat类

public static class Zoo {

    private String name;
    private AnimalTypeEnum animalType;

}

父类Zoo中,包含一个代表动物种类的枚举字段

public enum AnimalTypeEnum {
    DOG("dog"),
    CAT("cat");
    private final String name;
}

对于子类Dog包含一个速度属性

public static class Dog extends Zoo {
    private Double speed;
}

对于子类Cat包含一个尺寸属性

public static class Cat extends Zoo {
    private Integer size;
}

我们想做的事情是根据Zoo中的动物类型枚举字段animalType,将JSON反序列化为两种子类

使用Jackson提供的处理注解可以实现上述功能

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "animalType",
        visible = true
)
@JsonSubTypes(
        {
                @JsonSubTypes.Type(value = Dog.class, name = "DOG"),
                @JsonSubTypes.Type(value = Cat.class, name = "CAT")
        }
)
public static class Zoo {

    private String name;
    private AnimalTypeEnum animalType;

}

@JsonTypeInfo()

该注解表示对该类开启多态类型处理,包含四个属性

use 代表使用哪一种类型识别码

JsonTypeInfo.Id.NAME 是本例中选择的类型识别码,意指一个指定的名字

include代表识别码是如何包含进JSON

JsonTypeInfo.As.EXISTING_PROPERTY 代表POJO中存在的类型

property 指定类型表示码的属性名称

"animalType" 就是POJO中的类型字段名

visible 代表类型标识符是否会进入反序列化,默认false

由于这里我们同样需要该字段反序列化,所以设置为true

@JsonSubTypes()

该注解用于给出给定类的子类

@JsonSubTypes.Type[]数组中给出了多态类和property中指定属性某个值之间的绑定关系。在上例中,Dog类和animalType = DOG的值进行了绑定

在父类Zoo上加入如上注解之后,即可实现多态反序列化

对应测试

public void method1Test() throws JsonProcessingException {

    ObjectMapper objectMapper = new ObjectMapper();
    Cat cat = new Cat("小猫", AnimalTypeEnum.CAT, 20);
    Dog dog = new Dog("小狗", AnimalTypeEnum.DOG, 30.03);

    String catJson = objectMapper.writeValueAsString(cat);
    String dogJson = objectMapper.writeValueAsString(dog);

    log.debug(catJson);
    log.debug(dogJson);

    //反序列化
    Zoo catZoo = objectMapper.readValue(catJson, Zoo.class);
    Zoo dogZoo = objectMapper.readValue(dogJson, Zoo.class);

    //类型一致
    assertEquals(catZoo.getClass(),cat.getClass());
    assertEquals(dogZoo.getClass(),dog.getClass());

    //参数值一致
    assertEquals(20,((Cat)catZoo).getSize());
    assertEquals(30.03,((Dog)dogZoo).getSpeed());

}

![image-20210613223517026](/Users/liu/Library/Application Support/typora-user-images/image-20210613223517026.png)

可以看到,经过添加注解可以实现我们的需求

这样不管是springboot还是mybaitis-plus进行反序列化的时候,都通过注解的信息按照我们的要求进行反序列化

在项目中,一个基类会有很多的子类,并且随着项目的深入,子类可能会越来越多。使用上面的方法,需要不停的添加@JsonSubTypes中的内容,十分繁琐。这种写法是 违反开闭原则(OCP)的。

通过阅读源码,我们可以看到,其多态处理的基本原理就是将子类何其对应的名称之间的绑定关系注册到ObjectMapper中。

方法二的思路是给每个子类增加一个注解@JsonTypeName(value = ""),然后通过扫描所有带有注解的类,将所有带有标记的类注册到ObjectMapper中。

在Springboot中自定义ObjectMapper有很多办法,可以参考在SpringBoot中自定义 Jackson的ObjectMapper

首先生成一个ObjectMapper 的bean

@Configuration
public class ObjectMapperConfig {
    @Bean
    @Primary
    //使这个bean优先被注入
    public ObjectMapper objectMapper() {

        ObjectMapper objectMapper = new ObjectMapper();

        //使用reflection框架,获取本包下的所有带有@JsonTypeName的注解
        Reflections reflections = new Reflections("cn.");
        Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(JsonTypeName.class);
        //将这个上面扫描得到的类注册进这个ObjectMapper中
        objectMapper.registerSubtypes(classSet);

        //这里是将我们定义好的objectMapper set 进 Mybaitis-plus的Jackson处理器中,从而使得MP也可以                顺利的进行反序列化
        JacksonTypeHandler.setObjectMapper(objectMapper);
        return objectMapper;
    }
}

父类只需要添加这样一个注解

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "animalType",
        visible = true
)
public static class Zoo {

    private String name;
    private AnimalTypeEnum animalType;

}

子类添加注解

@JsonTypeName("DOG")
public static class Dog extends Zoo {
    private Double speed;
}

在我们的场景中,分类标识符是一个枚举类型。因此,我们希望将所有的子类和标识符名称对应的信息全部放在该枚举类中,使得仅通过枚举类就可以绑定好所有子类和名称之间的关系。

定义一个接口和注解,并在接口上使用了这个注解

@JsonSubTypeEnum.JsonSubTypeAnnotation
public interface JsonSubTypeEnum {

    Class<?> getJsonType();

    @Documented
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @interface JsonSubTypeAnnotation {
    }
}

这个接口定义了一个获取子类类信息的方法

public enum AnimalType implements JsonSubTypeEnum {
    DOG(Dog.class),
    CAT(Cat.class),
    ;
    private final Class<? extends Animal> animalClass;

    @Override
    public Class<?> getJsonType() {
        return this.animalClass;
    }
}

让需要用于分类的枚举实现这个接口,枚举中的animalClass属性,用来记录该标识符对应的子类的类别。

再来看ObjectMapper bean

@Bean
@Primary
public static ObjectMapper getObjectMapper(){
    ObjectMapper objectMapper = new ObjectMapper();
    Reflections reflections = new Reflections("com.");
  //获取所有带有自定义注解的类
    Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(JsonSubTypeEnum.JsonSubTypeAnnotation.class);
    //将其中的枚举类拿出来处理
      for (Class<?> enumTyp : classSet) {
        if (!enumTyp.isEnum()) {
            continue;
        }
        final Object[] enumConstants = enumTyp.getEnumConstants();
        for (Object e : enumConstants) {
            if (e instanceof JsonSubTypeEnum) {
                //将每个子类和标识符绑定注册进入objectMapper
                final Class<?> subType = ((JsonSubTypeEnum) e).getJsonType();
                final String name = ((Enum<?>) e).name();
                objectMapper.registerSubtypes(new NamedType(subType, name));
            }
        }
    }
         //这里是将我们定义好的objectMapper set 进 Mybaitis-plus的Jackson处理器中,从而使得MP也可以                顺利的进行反序列化
   JacksonTypeHandler.setObjectMapper(objectMapper);
   return objectMapper;
}