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

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

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


责任编辑:武晓燕 来源: geekhalo
相关推荐

2009-06-06 09:07:05

微软盖茨庄园

2019-10-09 13:39:39

Python编程语言异常错误

2023-08-29 17:40:28

技术大会

2018-04-27 14:18:01

2017-05-11 08:46:35

全闪存数据中心容量

2014-08-21 14:49:32

MIUI 6

2024-08-09 08:25:32

Spring流程注解

2009-06-12 16:55:10

VPN客户端故障

2019-08-22 14:02:00

Spring BootRestful APIJava

2010-11-19 17:11:48

RG-SSO组件五位一体锐捷网络

2023-12-05 14:10:00

接口可读性

2021-12-02 07:50:30

NFS故障内存

2024-01-29 09:22:59

死锁线程池服务

2022-01-17 09:19:12

Transformer数据人工智能

2020-05-26 13:48:05

后端框架异常

2020-02-19 08:00:00

微服务架构分布式代码

2021-04-14 17:47:12

联想

2012-08-17 09:32:50

虚拟化

2011-05-07 15:14:59

喷墨打印机故障维护

2017-10-26 09:46:50

点赞
收藏

51CTO技术栈公众号