1. 问题&分析
使用 code 真香,终于不用担心枚举重构了,但还是高兴的太早了,一个线上bug正在路上….
1.1. 案例
经过连续多天奋战,系统终于上线了订单手工取消功能,刚刚上线便收到客服部门的反馈:订单列表中订单状态出现问题,显示未 undefine。小艾赶紧查看后端日志,没有发现任何异常,并紧急给前端负责人虎哥挂了个电话,很快虎哥便定位原因并进行紧急修复。
事后复盘,原因是这样的:
后端返回结果如下图所示:
图片
默认情况下,枚举只会返回 Name,非常不利于展示,所以在前端会进行一次翻译,将 Name 翻译成展示文案。
在这个接口的基础上引起的问题如下图所示:
图片
由于业务发展,OrderStatus 的枚举值发生了变化,但只对主站页面进行调整,而客服系统被遗漏。所以:
1.2. 问题分析
深入思考,该问题的本质就是:对信息没有进行统一维护,导致同一份数据在多个地方进行管理,当发生变化时只要有一处未及时更新便会出现问题。
那解法也就很简单了,将信息收口到后端进行统一管理!
除这个问题外,还有一个非常类似的问题:前端下拉列表,也需要和后端定义保持一致,一般情况下:
2. 解决方案
和 code 方案一致,可以使用接口对枚举进行约束。
2.1. 构建统一接口
首先,定义统一的接口,用于提供描述信息:
public interface SelfDescribedEnum {
default String getName(){
return name();
}
String name();
/**
* 获取描述信息
*/
String getDescription();
}
2.2. 枚举实现接口
然后,让我们的枚举实现 SelfDescribedEnum 接口,具体如下:
public enum SelfDescribedEnumBasedOrderStatus implements SelfDescribedEnum {
CREATED("待支付"),
TIMEOUT_CANCELLED("超时取消"),
MANUAL_CANCELLED("手工取消"),
PAID("支付成功"),
FINISHED("已完成");
private final String description;
SelfDescribedEnumBasedOrderStatus(String description) {
this.description = description;
}
@Override
public String getDescription() {
return description;
}
}
2.3. 集成 Spring MVC 返回结果
在完成上述工作后,我们将 OrderVO 中的 status 属性类型更新为 SelfDescribedEnumBasedOrderStatus,具体如下:
@Data
public class OrderVO {
private Long id;
private SelfDescribedEnumBasedOrderStatus status;
}
最后一步也是最关键的一步便是,对 Jackson 序列化器进行定制,核心代码如下:
@Configuration
public class SelfDescribedEnumJacksonCustomizer {
@Bean
public Jackson2ObjectMapperBuilderCustomizer commonEnumBuilderCustomizer(){
return builder ->{
// 注册自定义枚举序列化器
builder.serializerByType(SelfDescribedEnum.class, new SelfDescribedEnumJsonSerializer());
};
}
static class SelfDescribedEnumJsonSerializer extends JsonSerializer {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
SelfDescribedEnum selfDescribedEnum = (SelfDescribedEnum) o;
SelfDescribedEnumVO selfDescribedEnumVO = SelfDescribedEnumVO.from(selfDescribedEnum);
jsonGenerator.writeObject(selfDescribedEnumVO);
}
}
}
// SelfDescribedEnumVO 为定义的一个 VO,具体如下:
@Data
public class SelfDescribedEnumVO {
@ApiModelProperty(notes = "Name")
private final String name;
@ApiModelProperty(notes = "描述")
private final String desc;
public static SelfDescribedEnumVO from(SelfDescribedEnum selfDescribedEnum){
if (selfDescribedEnum == null){
return null;
}
return new SelfDescribedEnumVO(selfDescribedEnum.getName(), selfDescribedEnum.getDescription());
}
public static List from(List commonEnums){
if (CollectionUtils.isEmpty(commonEnums)){
return Collections.emptyList();
}
return commonEnums.stream()
.filter(Objects::nonNull)
.map(SelfDescribedEnumVO::from)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
最后,启动服务查看新返回值,具体如下:
图片
可以看,status 字段原本只返回了 name,现在返回的是一个包括 name 和 desc 的对象。前端无需进行转换,只需直接读取 status.desc 信息即可。
2.4. 提供统一字典服务
对于下来列表、选择框的场景,最优方案是为前端提供一个统一的字典接口,由该接口来返回所有字典信息。
核心代码如下:
public class EnumDictController {
private Map enumDict = new HashMap();
public EnumDictController(){
add("OrderStatus", SelfDescribedEnumBasedOrderStatus.values());
}
private void add(String type, SelfDescribedEnumBasedOrderStatus[] values) {
this.enumDict.put(type, Arrays.asList(values));
}
/**
* 获取所有字典信息
* @return
*/
@GetMapping("all")
public RestResult allEnums(){
Map dictVo = Maps.newHashMapWithExpectedSize(enumDict.size());
for (Map.Entry entry : enumDict.entrySet()){
dictVo.put(entry.getKey(), SelfDescribedEnumVO.from(entry.getValue()));
}
return RestResult.success(dictVo);
}
/**
* 获取支持的全部字典类型
* @return
*/
@GetMapping("types")
public RestResult enumTypes(){
return RestResult.success(Lists.newArrayList(enumDict.keySet()));
}
/**
* 获取指定字典的全部值
* @param type
* @return
*/
@GetMapping("/{type}")
public RestResult dictByType(@PathVariable("type") String type){
List enums = enumDict.get(type);
return RestResult.success(SelfDescribedEnumVO.from(enums));
}
}
启动服务,验证字典接口。
获取全部字典信息,返回结果如下:
图片
一次性返回全部字典对性能有损耗,那可以返回指定字典,结果如下:
图片
此时,前端只需从接口中获取所需要的数据,无需在 js 中进行单独维护。
3. 示例&源码
代码仓库:https://gitee.com/litao851025/learnFromBug
代码地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/descr