Jackson反序列化多态

Jackson反序列化多态

1.前置知识

@JsonTypeInfo作用于类/接口,被用来开启多态类型处理,对基类/接口和子类/实现类都有效。

注解属性:

use:定义使用哪一种类型识别码,它有下面几个可选值:

  • JsonTypeInfo.Id.CLASS:使用完全限定类名做识别
  • JsonTypeInfo.Id.MINIMAL_CLASS:若基类和子类在同一包类,使用类名(忽略包名)作为识别码
  • JsonTypeInfo.Id.NAME:一个合乎逻辑的指定名称
  • JsonTypeInfo.Id.CUSTOM:自定义识别码,由@JsonTypeIdResolver对应,稍后解释
  • JsonTypeInfo.Id.NONE:不使用识别码

include(可选):指定识别码是如何被包含进去的,它有下面几个可选值:

  • JsonTypeInfo.As.PROPERTY:作为数据的兄弟属性
  • JsonTypeInfo.As.EXISTING_PROPERTY:作为POJO中已经存在的属性
  • JsonTypeInfo.As.EXTERNAL_PROPERTY:作为扩展属性
  • JsonTypeInfo.As.WRAPPER_OBJECT:作为一个包装的对象
  • JsonTypeInfo.As.WRAPPER_ARRAY:作为一个包装的数组

property(可选):制定识别码的属性名称
此属性只有当:

  • useJsonTypeInfo.Id.CLASS(若不指定property则默认为@class,在序列化时自定注入)、JsonTypeInfo.Id.MINIMAL_CLASS(若不指定property则默认为@c,,在序列化时自定注入)、JsonTypeInfo.Id.NAME(若不指定property默认为@type),
  • includeJsonTypeInfo.As.PROPERTYJsonTypeInfo.As.EXISTING_PROPERTYJsonTypeInfo.As.EXTERNAL_PROPERTY时才有效

@JsonSubTypes作用于类/接口,用来列出给定类的子类,只有当子类类型无法被检测到时才会使用它,一般是配合@JsonTypeInfo在基类上使用。

@JsonSubTypes的值是一个@JsonSubTypes.Type[]数组,里面枚举了多态类型(value对应子类)和类型的标识符值(name对应@JsonTypeInfo中的property标识名称的值,此为可选值,若不制定需由@JsonTypeName在子类上制定)

2.需求背景

Qring需要创建一个优惠券模板,在创建的时候希望能够只通过调用一个接口方法即可,不希望记住太多的方法接口,在创建的时候会传入不同的模板信息。

3.分析

根据面向对象思想我们抽象出了一个优惠券信息父类,在它的下面有不同的子类,比如折扣券、礼品券,我们定了两个接口,提供给Qring创建券模板以及获取券模板信息。

在创建券模板时,我们只有一个创建券模板的接口,而券模板里面只放着优惠券信息的父类,如果不进行标识,Jackson是无法知道我们究竟需要创建的是哪一种券的券模板,因此我们需要使用注解@JsonTypeInfo为优惠券信息父类打上标识告诉Jackson在反序列化时究竟要怎么做,同时使用@JsonSubTypes说明在优惠券信息父类下有哪些子类。

在获取券模板信息时,我们需要配合@TableName(autoResultMap = true)以及@TableField(typeHandler = JacksonTypeHandler.class)让mybatis-plus知道如何进行反序列化。

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
@JsonSubTypes(value = {
        @JsonSubTypes.Type(value = GiftCoupon.class, name = "GiftCoupon"),
        @JsonSubTypes.Type(value = DiscountCoupon.class, name = "DiscountCoupon"),
})
public abstract class Coupon {

}

@Data
public class DiscountCoupon extends Coupon {
    private String type;
    private Double discount;
}

@Data
public class GiftCoupon extends Coupon {
    private String type;
    private String giftName;
    private Integer value;
}
@Data
@TableName(value = "coupon_template", autoResultMap = true)
public class CouponTemplate {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @TableField(value = "coupon", typeHandler = JacksonTypeHandler.class)
    private Coupon coupon;
}
	@GetMapping("/getCouponTemplate")
    ResultData<CouponTemplate> getCouponTemplate(@RequestParam Integer id) {
        return ResultData.success(couponTemplateMapper.selectById(id));
    }

    @PostMapping("/createCouponTemplate")
    ResultData<Integer> getCouponTemplate(@RequestBody CouponTemplate couponTemplate) {
        return ResultData.success(couponTemplateMapper.insert(couponTemplate));
    }

4.一些小问题

如果@JsonTypeInfo的include使用的不是JsonTypeInfo.As.EXISTING_PROPERTY,那么在反序列时返回的数据会出现问题,标记字段会出现两次,如果设置visible = false,那么其中一个会为null,如果从数据库查询直接返回给前端可能会出现问题。

{
  "status": 100,
  "message": "操作成功",
  "data": {
    "id": 5,
    "coupon": {
      "type": "DiscountCoupon",
      "type": "DiscountCoupon",
      "discount": 0.95
    }
  },
  "timestamp": 1657607815210
}

跟踪ObjectMapper源码时发现,如果是JsonTypeInfo.As.EXISTING_PROPERTY,那么在JsonGenerator.writeTypePrefix()的swtich时走的是PAYLOAD_PROPERTY分支,不进行任何处理,而其他类型会进行相应的处理,如JsonTypeInfo.As.EXTERNAL_PROPERTY会将type标记字段进行写入,之后再将bean的属性写入,因此出现了两个相同的字段。具体过程可跟踪ObjectMapper源码进行深入了解。

public WritableTypeId writeTypePrefix(WritableTypeId typeIdDef) throws IOException {
    Object id = typeIdDef.id;
    JsonToken valueShape = typeIdDef.valueShape;
    if (this.canWriteTypeId()) {
        typeIdDef.wrapperWritten = false;
        this.writeTypeId(id);
    } else {
        String idStr = id instanceof String ? (String)id : String.valueOf(id);
        typeIdDef.wrapperWritten = true;
        Inclusion incl = typeIdDef.include;
        if (valueShape != JsonToken.START_OBJECT && incl.requiresObjectContext()) {
            typeIdDef.include = incl = Inclusion.WRAPPER_ARRAY;
        }

        switch(incl) {
            case PARENT_PROPERTY:
            case PAYLOAD_PROPERTY:
                break;
            case METADATA_PROPERTY:
                this.writeStartObject(typeIdDef.forValue);
                this.writeStringField(typeIdDef.asProperty, idStr);
                return typeIdDef;
            case WRAPPER_OBJECT:
                this.writeStartObject();
                this.writeFieldName(idStr);
                break;
            case WRAPPER_ARRAY:
            default:
                this.writeStartArray();
                this.writeString(idStr);
        }
    }

    if (valueShape == JsonToken.START_OBJECT) {
        this.writeStartObject(typeIdDef.forValue);
    } else if (valueShape == JsonToken.START_ARRAY) {
        this.writeStartArray();
    }

    return typeIdDef;
}

你可能感兴趣的:(常见问题,java,Jackson,反序列化,Mybatis-plus)