1. 问题&分析
使用 code 真香,终于不用担心枚举重构了,但还是高兴的太早了,一个线上bug正在路上….
1.1. 案例
经过连续多天奋战,系统终于上线了订单手工取消功能,刚刚上线便收到客服部门的反馈:订单列表中订单状态出现问题,显示未 undefine。小艾赶紧查看后端日志,没有发现任何异常,并紧急给前端负责人虎哥挂了个电话,很快虎哥便定位原因并进行紧急修复。
事后复盘,原因是这样的:
- 在订单列表接口中,后端只返回了枚举的 name
- 前端维护了一个配置文件,key 是 name,value 是显示名称,从接口获取 name 后会基于配置文件进行转换,最终展示为 描述信息
- 本次修改,只改了主站的 js 配置,遗漏了客服系统。所以,主站没有问题,而客服系统由于找不到新加的name,所以展示为 undefine
后端返回结果如下图所示:
图片
默认情况下,枚举只会返回 Name,非常不利于展示,所以在前端会进行一次翻译,将 Name 翻译成展示文案。
在这个接口的基础上引起的问题如下图所示:
图片
由于业务发展,OrderStatus 的枚举值发生了变化,但只对主站页面进行调整,而客服系统被遗漏。所以:
- 主站页面有最新的全量配置,信息展示准确没有任何问题
- 客服系统由于被遗忘使用的还是之前的配置,导致后端返回的 Name 和 配置信息不一致,由于找不到 Name 而出现 undefine 错误
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<SelfDescribedEnumVO> from(List<SelfDescribedEnum> 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<String, List<SelfDescribedEnum>> enumDict = new HashMap<String, List<SelfDescribedEnum>>();
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<Map<String, List<SelfDescribedEnumVO>>> allEnums(){
Map<String, List<SelfDescribedEnumVO>> dictVo = Maps.newHashMapWithExpectedSize(enumDict.size());
for (Map.Entry<String, List<SelfDescribedEnum>> entry : enumDict.entrySet()){
dictVo.put(entry.getKey(), SelfDescribedEnumVO.from(entry.getValue()));
}
return RestResult.success(dictVo);
}
/**
* 获取支持的全部字典类型
* @return
*/
@GetMapping("types")
public RestResult<List<String>> enumTypes(){
return RestResult.success(Lists.newArrayList(enumDict.keySet()));
}
/**
* 获取指定字典的全部值
* @param type
* @return
*/
@GetMapping("/{type}")
public RestResult<List<SelfDescribedEnumVO>> dictByType(@PathVariable("type") String type){
List<SelfDescribedEnum> 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