在使用枚举类做参数时,一般会让前端传数字,后端将数字转为枚举类,当枚举类很多时,很可能不知道这个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) {// 根据namereturn _fromString(p, ctxt, p.getText());}// 根据ordinalreturn _fromInteger(p, ctxt, p.getIntValue());}if (p.isExpectedStartObjectToken()) {return _fromString(p, ctxt,ctxt.extractScalarFromObject(p, this, _valueClass));}return _deserializeOther(p, ctxt);}
比如我们有一个接口:
@PostMapping("/product")@ResponseBodypublic 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> {@Overridepublic 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")@ResponseBodypublic 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);}@Overridepublic 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;}@Overridepublic 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方法@Overridepublic 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反序列化了。