太实用了!这几个方案解决枚举类Value和Label之间映射

起因
项目中存在非常非常多的枚举 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
来源:掘金

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342