在Springboot + Mybaitis-plus 專案中利用Jackson實現json對java多型的(反)序列化

LRY的爛筆頭 發表於 2021-07-22
人工智慧 Java Spring

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

可以看到,經過新增註解可以實現我們的需求

這樣不管是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;
}