【故障现场将变更收敛在一处,避免散弹式更新

2023年 12月 28日 98.3k 0

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 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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论