Spring后端直接用枚举类接收参数,自定义通用枚举类反序列化器
在使用枚举类做参数时,一般会让前端传数字,后端将数字转为枚举类,当枚举类很多时,很可能不知道这个code该对应哪个枚举类。能不能后端直接使用枚举类接收参数呢,可以,但是受限。
Spring反序列默认使用的是Jacskon,反序列化枚举类时,可以根据枚举类的name或ordinal属性进行反序列化,这是因为Jacskon内置了EnumDeserializer,它可以根据name或ordinal属性进行反序列化。
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
if (p.hasToken(JsonToken.VALUE_STRING)) {
return _fromString(p, ctxt, p.getText());
}
// But let's consider int acceptable as well (if within ordinal range)
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
if (_isFromIntValue) {
// 根据name
return _fromString(p, ctxt, p.getText());
}
// 根据ordinal
return _fromInteger(p, ctxt, p.getIntValue());
}
if (p.isExpectedStartObjectToken()) {
return _fromString(p, ctxt,
ctxt.extractScalarFromObject(p, this, _valueClass));
}
return _deserializeOther(p, ctxt);
}
比如我们有一个接口:
@PostMapping("/product")
@ResponseBody
public void product(@RequestBody Product product) {
System.out.println(product.getStatus());
System.out.println("ok");
}
public class Product {
private Status status;
private String name;
// getter and setter
}
public enum Status {
ON_LINE(1000, "在线"),
OFF_LINE(2000, "下线")
;
private int code;
private String desc;
Status(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
PostMan请求
这两种写法都可以。
status使用数字方式,只能是0或1,即枚举类的ordinal值.但是这种方式和我们的使用习惯不同,我们一般会自定义code,而不是使用ordinal。如果向根据自定义code反序列化枚举类,该如何实现呢?
参照Spring的方式,大概思路应该是自定义一个反序列化器,然后再需要使用自定义反序列化器的对象上加上@JsonDeserialize以覆盖Spring默认使用的EnumDeserializer。
自定义枚举类反序列化器
public class StatusDeser extends JsonDeserializer<Status> {
@Override
public Status deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
final int code = p.getIntValue();
return Status.getByCode(code);
}
}
给Status增加一个getByCode,且指定反序列化器。
@JsonDeserialize(using = StatusDeser.class)
public enum Status {
ON_LINE(1000, "在线"),
OFF_LINE(2000, "下线")
;
private int code;
private String desc;
Status(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static Status getByCode(int code) {
final Status[] values = Status.values();
for (int i = 0; i < values.length; i++) {
if (values[i].code == code) {
return values[i];
}
}
throw new RuntimeException("不合法的code值");
}
}
此时就可以这样传值了
注意,如果直接使用枚举类做接收参数,接口和body应该这样写
@PostMapping("/status")
@ResponseBody
public void status(@RequestBody Status status) {
System.out.println(status);
System.out.println("ok");
}
统一处理枚举类
上面自定义的反序列化器可以处理某一种枚举,如果枚举很多,每写一个枚举类都要写一个与之对应的反序列化器,有点麻烦,而且一旦忘记写或不知道要写就麻烦了。如何对枚举类做统一处理呢?可以让枚举类都继承一个接口,我们的反序列化器对接口类型处理。
定义统一接口
public interface BaseEnum {
Integer getCode();
}
统一接口中有一个返回code值的方法,前端传这个code值,我们根据这个code值反序列化出对应的枚举对象。
定义统一反序列化器
因为我们的接口下可以有多种实现类枚举,那我们在反序列化的时候要反序列化成哪种枚举类呢?怎么在运行时知道我们的目标枚举类呢?这个时候要使用StdDeserializer和ContextualDeserializer。
StdDeserializer中有目标对象类型,ContextualDeserializer可以在反序列化时获取目标对象类型信息。两者结合就可以在运行时获取到目标对象类型信息,动态创建反序列化器。
public class BaseEnumDeserial extends StdDeserializer<BaseEnum>
implements ContextualDeserializer {
protected BaseEnumDeserial(Class<?> vc) {
super(vc);
}
// Jackson通过反射创建BaseEnumDeserial对象,这个对象不会用于正真的反序列化,因为它没有真实的类信息
// 真正用于反序列化的对象会通过createContextual重新创建
public BaseEnumDeserial() {
this(BaseEnum.class);
}
@Override
public BaseEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
// 反序列目标对象继承自BaseEnum且是枚举类型
if (BaseEnum.class.isAssignableFrom(_valueClass) && Enum.class.isAssignableFrom(_valueClass)) {
final int code = p.getIntValue();
BaseEnum[] enumConstants = (BaseEnum[]) _valueClass.getEnumConstants();
for (int i = 0; i < enumConstants.length; i++) {
if (code == enumConstants[i].getCode()) {
return enumConstants[i];
}
}
}
return null;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
// 从上下文获取目标对象类型
final Class<?> rawClass = ctxt.getContextualType().getRawClass();
// new出来的反序列化器不用我们缓存,一种类型的反序列化器的createContextual方法只会执行一次,执行后的结果Jackson自己会缓存
return new BaseEnumDeserial(rawClass);
}
}
注意,有的地方文章使用property获取目标对象类型,只有在枚举类作为其他对象的属性被反序列化时,property才有值,如果时直接反序列化枚举对象,则property是null,所以还是直接从上下文中取类型比较好。
// property可能为null
final Class<?> rawClass = property.getType().getRawClass();
枚举类改造
// 反序列器加在接口上,就不用在每个枚举类上加了
@JsonDeserialize(using = BaseEnumDeserial.class)
public interface BaseEnum {
Integer getCode();
}
// 实现BaseEnum 接口
public enum Status implements BaseEnum {
ON_LINE(1000, "在线"),
OFF_LINE(2000, "下线")
;
private int code;
private String desc;
Status(int code, String desc) {
this.code = code;
this.desc = desc;
}
// 删除了getByCode方法
@Override
public Integer getCode() {
return code;
}
}
定义接口
@PostMapping("/product")
@ResponseBody
// 枚举类在其他对象中
public void product(@RequestBody Product product) {
System.out.println(product.getStatus());
System.out.println("ok");
}
@PostMapping("/status")
@ResponseBody
// 直接反序列化枚举类
public void status(@RequestBody Status status) {
System.out.println(status);
System.out.println("ok");
}
此时,就可以前端传数字,后端直接用枚举类型接收,不用再做数字到枚举类型的转化了。改造之后,下面这种方式就不行了,只能使用code反序列化,而不能使用name反序列化了。