起因
项目中存在非常非常多的枚举 label 和 value 之间的映射场景,比如:实体类中存在 scene 字段,值可以枚举,分别为 INSURANCE(保险)、OFFLINE(线下)、OTHER(其他)。
当此字段出现在查询接口返回中,需要将对应的中文返回给前端显示。当此字段出现在请求保存接口中,前端会将值上传到后端接口中,后端需要校验上传的值。
之前做法是,对校验方法进行封装,使用 if 编码判断该字段是否需要进行枚举类的映射和校验。项目代码量增加,冗余代码越来越多。
设想是,使用注解,在单个字段上加上注解就能够实现校验和映射。如下:
public class TestBean {
@EnumCheck(target = MchSceneEnum.class) // 校验枚举值是否合法
@EnumLabel(target = MchSceneEnum.class) // 输出到前端时映射为 label 文字
private String scene;
}
处理枚举值输入校验
结合 Spring Validate 技术,定义自定义注解,创建注解校验器,如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// validatedBy 属性与校验器 EnumCheckValidator 相关联
@Constraint(validatedBy = {EnumCheckValidator.class})
public @interface EnumCheck {
// 接收枚举类型
Class<?> target();
// 非必填字段,用于 spring validate
String regexp() default "";
String message() default "值必须在枚举值内中选填";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
相应的校验器
public class EnumCheckValidator implements ConstraintValidator<EnumCheck, String> {
Class targetClass; @Override
public void initialize(EnumCheck constraintAnnotation) {
targetClass = constraintAnnotation.target(); } @Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (!targetClass.isEnum()) {
return false;
} // 枚举类的成员
Object[] enumInstances = targetClass.getEnumConstants();
// 枚举类的所有 value 值集合
Set<String> values = new HashSet<>();
try {
for (Object enumInstance : enumInstances) {
Method getValue = targetClass.getMethod("getValue");
Object valueObj = getValue.invoke(enumInstance, null);
values.add(valueObj.toString());
}
}
catch (Exception e) {
return false;
}
return values.contains(value);
}
}
要求枚举类中需要有 getValue 方法,大概如下:
public enum MchSceneEnum implements LabelAndValue<String> {
INSURANCE("INSURANCE", "保险"),
OTHER("OTHER", "非保险");
MchSceneEnum(String value, String label){
this.value = value;
this.label = label;
} private String value;
private String label;
@Override
public String getValue() {
return this.value;
} @Override
public String getLabel() {
return this.label;
}}
使用示例: (注:本文省略 Spring Validate 入门介绍,默认已支持该框架)
// 测试 vo,用于接收前端输入的 scene 值
public class TestReqVo { @EnumCheck(target = MchSceneEnum.class) private String scene;}// 测试 Controller@RequestMapping("/test")
@RestControllerpublic class TestController { // org.springframework.validation.annotation.Validated
public Stirng void add(@Validated TestVo vo) { return ""; // 逻辑省略
}}
添加 Validated 注解后,Spring 框架会接收到前端参数后,自动校验输入值。规则即为上文所定义的校验器逻辑。
Spring Validate 框架在校验失败时,默认抛出 org.springframework.validation.BindException 异常。为优雅地处理该异常,可结合 Spring 全局异常处理技术(@ControllerAdvice),大致处理如下:
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(BindException.class)
public String handleBindException(BindException ex) {
BindingResult result = ex.getBindingResult(); if (result.hasErrors()) {
FieldError fieldError = result.getFieldError(); } return fieldError.getDefaultMessage(); // 注解定义的提示文字
}
处理枚举值输出映射
方案一:Controller 层处理:jackson
前后端交互时,若是查询接口,前端要求的是枚举值的 label,用于展示。如 scene 字段值为 INSURENCE 时,前端需要展现的是保险两字。
一开始想到利用 jackson 框架的 @JsonSerialize 注解,但是此注解不能再额外增加自己定义的参数,在本文中就是需要序列化的对应的枚举类型,拿不到此参数就不知道需要输出什么 label。
接着尝试了 MappingJackson2HttpMessageConverter 中注入自定义的 objectMapper 来实现(继承),但是此种方法也不能拿到字段上面的自定义注解。
最后在网上终于找到了资料:Jackson使用ContextualSerializer在序列化时获取字段注解的属性
解决了获取字段上面自定义注解问题,就可以开始编码实现了,先定义注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = EnumSerializer.class)
public @interface EnumLabel {
Class<?> target();}
将 JsonSerialize 作用于在自定义注解上,当使用 EnumLabel 注解时,也相当于使用了 JsonSerialize 注解。using 属性值填自定义序列化器的类型。EnumSerializer 定义如下:
public class EnumSerializer extends JsonSerializer<Object> implements ContextualSerializer {
private Map<String, String> valueLabelMap;
public EnumSerializer(Map<String, String> valueLabelMap) {
this.valueLabelMap = valueLabelMap;
} @Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
String actValue = (String) value;
if (this.valueLabelMap != null) {
actValue = valueLabelMap.getOrDefault(value, value.toString()); } jgen.writeString(actValue); } @Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) { // 为空直接跳过
// 只对 String 类型生效
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
EnumLabel enumLabel = beanProperty.getAnnotation(EnumLabel.class);
if (enumLabel != null) {
// key:getValue value:getLabel
Object[] enumInstances = enumLabel.target().getEnumConstants(); // 枚举类的成员
Map<String, String> map = new HashMap<>(enumInstances.length);
try {
for (Object enumInstance : enumInstances) {
Method getValue = enumLabel.target().getMethod("getValue");
Object valueObj = getValue.invoke(enumInstance);
Method getLabel = enumLabel.target().getMethod("getLabel");
Object labelObj = getLabel.invoke(enumInstance);
map.put(valueObj.toString(), labelObj.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
return new EnumSerializer(map);
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(beanProperty);
}
}
示例中的 createContextual 方法只会被调用一次,所以对性能不会有影响。
自定义注解使用方法如下:target 中填对应的枚举类型
public class TestRespVo {
@EnumLabel(target = MchSceneEnum.class)
private String scene;
}
若查询到数据值为 "INSURANCE",当 Spring 输出到前端时,会将该值对应的中文描述重新写入,即 "保险"。
方案二:Dao 层处理:Mybatis
此方案我暂时没有采用,因为对参数的类型有要求,pojo 类中的字段类型必须为对应的枚举,与方案一相比,此方案较不灵活。
作者:午饭吃什么
链接:https://juejin.im/post/6844904058453491719
来源:掘金