【故障现场】将变更收敛在一处,避免散弹式更新
创始人
2025-07-09 22:01:34
0

1. 问题&分析

使用 code 真香,终于不用担心枚举重构了,但还是高兴的太早了,一个线上bug正在路上….

1.1. 案例

经过连续多天奋战,系统终于上线了订单手工取消功能,刚刚上线便收到客服部门的反馈:订单列表中订单状态出现问题,显示未 undefine。小艾赶紧查看后端日志,没有发现任何异常,并紧急给前端负责人虎哥挂了个电话,很快虎哥便定位原因并进行紧急修复。

事后复盘,原因是这样的:

  1. 在订单列表接口中,后端只返回了枚举的 name
  2. 前端维护了一个配置文件,key 是 name,value 是显示名称,从接口获取 name 后会基于配置文件进行转换,最终展示为 描述信息
  3. 本次修改,只改了主站的 js 配置,遗漏了客服系统。所以,主站没有问题,而客服系统由于找不到新加的name,所以展示为 undefine

后端返回结果如下图所示:

图片图片

默认情况下,枚举只会返回 Name,非常不利于展示,所以在前端会进行一次翻译,将 Name 翻译成展示文案。

在这个接口的基础上引起的问题如下图所示:

图片图片

由于业务发展,OrderStatus 的枚举值发生了变化,但只对主站页面进行调整,而客服系统被遗漏。所以:

  1. 主站页面有最新的全量配置,信息展示准确没有任何问题
  2. 客服系统由于被遗忘使用的还是之前的配置,导致后端返回的 Name 和 配置信息不一致,由于找不到 Name 而出现 undefine 错误

1.2. 问题分析

深入思考,该问题的本质就是:对信息没有进行统一维护,导致同一份数据在多个地方进行管理,当发生变化时只要有一处未及时更新便会出现问题。

那解法也就很简单了,将信息收口到后端进行统一管理!

除这个问题外,还有一个非常类似的问题:前端下拉列表,也需要和后端定义保持一致,一般情况下:

  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


相关内容

热门资讯

如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
施耐德电气数据中心整体解决方案... 近日,全球能效管理专家施耐德电气正式启动大型体验活动“能效中国行——2012卡车巡展”,作为该活动的...
Windows恶意软件20年“... 在Windows的早期年代,病毒游走于系统之间,偶尔删除文件(但被删除的文件几乎都是可恢复的),并弹...
规避非法攻击 用好路由器远程管... 单位在市区不同位置设立了科技服务点,每一个服务点的员工都通过宽带路由器进行共享上网,和单位网络保持联...
20个非常棒的扁平设计免费资源 Apple设备的平面图标PSD免费平板UI 平板UI套件24平图标Freen平板UI套件PSD径向平...
范例解读VB.NET获取环境变... VB.NET编程语言的使用范围非常广泛,可以帮助开发人员处理各种程序中的需求,而且还能对移动设备进行...
德国电信门户网站可实时显示全球... 德国电信周三推出一个门户网站,直观地实时提供其安装在全球各地的传感器网络检测到的网络攻击状况。该网站...