当前位置:首页 > 科技  > 软件

【故障现场】控制好取值范围,甭给别人犯错的机会

来源: 责编: 时间:2023-12-11 09:25:13 158观看
导读1. 问题&分析1.1. 案例小艾刚刚和大飞哥炒了一架,心情非常低落。整个事情是这样,小艾前段时间刚刚接手订单系统,今天收到一大波线上 NPE (Null Pointer Exception)报警,经排查发现订单表的商品类型(ProductType)出现一组非法

1. 问题&分析

1.1. 案例

小艾刚刚和大飞哥炒了一架,心情非常低落。整个事情是这样,小艾前段时间刚刚接手订单系统,今天收到一大波线上 NPE (Null Pointer Exception)报警,经排查发现订单表的商品类型(ProductType)出现一组非法值,在展示订单时由于系统无法识别这些非法值导致空指针异常。小艾通过排查,发现订单来自于市场团队,于是找到团队负责人大飞哥,并把现状和排查结果进行同步。经过大飞哥的排查,确实是在前端的各种跳转过程中导致 商品类型参数 被覆盖,立即安排紧急上线进行修复。整个事情处理速度快也没造成太大损失,但在事故复盘过程中出现了偏差:SNr28资讯网——每日最新资讯28at.com

  1. 小艾认为核心问题是调用方没有按规范进行传参,所以主要责任在大飞哥;
  2. 大飞哥则认为是订单系统未对输入参数进行有效性校验,致使问题数据存储至数据库,才出现后续的各种问题,所以主要责任在小艾;

两人各持己见争论不休,你认为责任在谁呢?SNr28资讯网——每日最新资讯28at.com

1.2. 问题分析

在订单系统中,商品类型定义为 Integer 类型,使用静态常量来表示系统所支持的具体值,核心代码如下:SNr28资讯网——每日最新资讯28at.com

// 领域对象public class OrderItem{    private Integer productType;}// 定义 ProductTypes 管理所有支持的 ProductTypepublic class ProductTypes{    public static final Integer CLAZZ = 1;    public static final Integer BOOK = 2;    // 其他类型}// 创建订单的请求对象@Data@ApiModel(description = "创建单个订单")class CreateOrderRequest {    @ApiModelProperty(value = "产品类型")    private Integer productType;    @ApiModelProperty(value = "产品id")    private Integer productId;    @ApiModelProperty(value = "数量")    private Integer amount;}

对应的 Swagger 如下:SNr28资讯网——每日最新资讯28at.com

图片图片SNr28资讯网——每日最新资讯28at.com

由于类型定义为 Integer, 所以当输入非法值(ProductTypes 定义之外的值)时,系统仍旧能接受并执行后续流程,这就是最核心的问题所在,如下图所示:SNr28资讯网——每日最新资讯28at.com

图片图片SNr28资讯网——每日最新资讯28at.com

==商品类型(ProductType)在系统中是一个字典,有自己的固定取值范围==,定义为 Integer 将放大可接受的值,一旦值在 ProductType 之外便会发生系统异常。SNr28资讯网——每日最新资讯28at.com

2. 解决方案

针对这个案例,小艾可以基于 ProductTypes 中定义的常量对所有入参进行校验,并在接入文档中进行强调。但,随着系统的发展肯定会加入更多的流程,在新流程中产生遗漏就又会出现同样的问题,那终极解决方案是什么?SNr28资讯网——每日最新资讯28at.com

将 ProductType 可接受的取值范围与类型的取值范围保存一致!!!SNr28资讯网——每日最新资讯28at.com

图片图片SNr28资讯网——每日最新资讯28at.com

这正是枚举重要的应用场景。SNr28资讯网——每日最新资讯28at.com

【原则】规范、流程 在没有检测机制相辅助时都不可靠。如有可能,请使用编译器进行强制约束!!!SNr28资讯网——每日最新资讯28at.com

2.1. 枚举基础知识

关键词 enum 可以将一组具名值的有限集合创建成一种新的类型,而这些具名的值可以作为常规程序组件使用。SNr28资讯网——每日最新资讯28at.com

枚举最常见的用途便是==替换常量定义==,为其增添类型约束,完成编译时类型验证。SNr28资讯网——每日最新资讯28at.com

2.1.1 枚举定义

枚举的定义与类和常量定义非常类似。使用 enum 关键字替换 class 关键字,然后在 enum 中定义“常量”即可。SNr28资讯网——每日最新资讯28at.com

对于 ProductType 枚举方案如下:SNr28资讯网——每日最新资讯28at.com

// 定义public enum ProductType {    CLAZZ, BOOK;}public class OrderItem{    private ProductType productType;}

getProductType 和 setProductType 所需类型为 ProductType,不在是比较宽泛的 Integer。在使用的时候可以通过 ProductType.XXX 的方式获取对应的枚举值,这样对类型有了更强的限制。SNr28资讯网——每日最新资讯28at.com

2.1.2. 枚举的单例性

枚举值具有单例性,及枚举中的每个值都是一个单例对象,可以直接使用 == 进行等值判断。SNr28资讯网——每日最新资讯28at.com

枚举是定义单例对象最简单的方法。SNr28资讯网——每日最新资讯28at.com

2.1.3. name 和 ordrial

对于简单的枚举,存在两个维度,一个是name,即为定义的名称;一个是ordinal,即为定义的顺序。SNr28资讯网——每日最新资讯28at.com

图片图片SNr28资讯网——每日最新资讯28at.com

简单测试如下:SNr28资讯网——每日最新资讯28at.com

@Testpublic void nameTest(){    for (ProductType productType : ProductType.values()){        // 枚举的name维度        String name = productType.name();        System.out.println("ProductType:" + name);        // 通过name获取定义的枚举        ProductType productType1 = ProductType.valueOf(name);        System.out.println(productType == productType1);    }}

输出结果为:SNr28资讯网——每日最新资讯28at.com

ProductType:CLAZZtrueProductType:BOOKtrue

ordrial测试如下:SNr28资讯网——每日最新资讯28at.com

@Testpublic void ordinalTest(){    for (ProductType productType : ProductType.values()){        // 枚举的ordinal维度        int ordinal = productType.ordinal();        System.out.println("ProductType:" + ordinal);        // 通过ordinal获取定义的枚举        ProductType productType1 = ProductType.values()[ordinal];        System.out.println(productType == productType1);    }}

输出结果如下:SNr28资讯网——每日最新资讯28at.com

ProductType:0trueProductType:1true

从输出上可以清晰的看出:SNr28资讯网——每日最新资讯28at.com

  1. name 是我们在枚举中定义变量的名称
  2. ordrial 是我们在枚举中定义变量的顺序

2.1.4. 枚举的本质

enum可以理解为编译器的语法糖,在创建 enum 时,编译器会为你生成一个相关的类,这个类继承自 java.lang.Enum。SNr28资讯网——每日最新资讯28at.com

先看下Enum提供了什么:SNr28资讯网——每日最新资讯28at.com

public abstract class Enum<E extends Enum<E>>        implements Comparable<E>, Serializable {    // 枚举的Name维度    private final String name;    public final String name() {        return name;    }    // 枚举的ordinal维度    private final int ordinal;    public final int ordinal() {        return ordinal;    }    // 枚举构造函数    protected Enum(String name, int ordinal) {        this.name = name;        this.ordinal = ordinal;    }    /**     * 重写toString方法, 返回枚举定义名称     */    public String toString() {        return name;    }    // 重写equals,由于枚举对象为单例,所以直接使用==进行比较    public final boolean equals(Object other) {        return this==other;    }    // 重写hashCode    public final int hashCode() {        return super.hashCode();    }    /**     * 枚举为单例对象,不允许clone     */    protected final Object clone() throws CloneNotSupportedException {        throw new CloneNotSupportedException();    }    /**     * 重写compareTo方法,同种类型按照定义顺序进行比较     */    public final int compareTo(E o) {        Enum<?> other = (Enum<?>)o;        Enum<E> self = this;        if (self.getClass() != other.getClass() && // optimization            self.getDeclaringClass() != other.getDeclaringClass())            throw new ClassCastException();        return self.ordinal - other.ordinal;    }    /**     * 返回定义枚举的类型     */    @SuppressWarnings("unchecked")    public final Class<E> getDeclaringClass() {        Class<?> clazz = getClass();        Class<?> zuper = clazz.getSuperclass();        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;    }    /**     * 静态方法,根据name获取枚举值     * @since 1.5     */    public static <T extends Enum<T>> T valueOf(Class<T> enumType,                                                String name) {        T result = enumType.enumConstantDirectory().get(name);        if (result != null)            return result;        if (name == null)            throw new NullPointerException("Name is null");        throw new IllegalArgumentException(            "No enum constant " + enumType.getCanonicalName() + "." + name);    }    protected final void finalize() { }    /**     * 枚举为单例对象,禁用反序列化     */    private void readObject(ObjectInputStream in) throws IOException,        ClassNotFoundException {        throw new InvalidObjectException("can't deserialize enum");    }    private void readObjectNoData() throws ObjectStreamException {        throw new InvalidObjectException("can't deserialize enum");    }}

从 Enum 中我们可以得到:SNr28资讯网——每日最新资讯28at.com

  1. Enum 中对 name 和 ordrial(final)的属性进行定义,并提供构造函数进行初始化
  2. 重写了equals、hashCode、toString方法,其中toString方法默认返回 name
  3. 实现了Comparable 接口,重写 compareTo,使用枚举定义顺序进行比较
  4. 实现了Serializable 接口,并重写禁用了clone、readObject 等方法,以保障枚举的单例性
  5. 提供 valueOf 方法使用反射机制,通过name获取枚举值

到此已经解释了枚举类的大多数问题,ProductType.values(), ProductType.CLAZZ, ProductType.BOOK,又是从怎么来的呢?这些是编译器为其添加的。SNr28资讯网——每日最新资讯28at.com

@Testpublic void enumTest(){    System.out.println("Fields");    for (Field field : ProductType.class.getDeclaredFields()){        field.getModifiers();        StringBuilder fieldBuilder = new StringBuilder();        fieldBuilder.append(Modifier.toString(field.getModifiers()))                .append(" ")                .append(field.getType())                .append(" ")                .append(field.getName());        System.out.println(fieldBuilder.toString());    }    System.out.println();    System.out.println("Methods");    for (Method method : ProductType.class.getDeclaredMethods()){        StringBuilder methodBuilder = new StringBuilder();        methodBuilder.append(Modifier.toString(method.getModifiers()));        methodBuilder.append(method.getReturnType())                .append(" ")                .append(method.getName())                .append("(");        Parameter[] parameters = method.getParameters();        for (int i=0; i< method.getParameterCount(); i++){            Parameter parameter = parameters[i];            methodBuilder.append(parameter.getType())                    .append(" ")                    .append(parameter.getName());            if (i != method.getParameterCount() -1) {                    methodBuilder.append(",");            }        }        methodBuilder.append(")");        System.out.println(methodBuilder);    }}

我们分别对 ProductType 中的属性和方法进行打印,结果如下:SNr28资讯网——每日最新资讯28at.com

Fieldspublic static final class com.example.enumdemo.ProductType CLAZZpublic static final class com.example.enumdemo.ProductType BOOKprivate static final class [Lcom.example.enumdemo.ProductType; $VALUESMethodspublic staticclass [Lcom.example.enumdemo.ProductType; values()public staticclass com.example.enumdemo.ProductType valueOf(class java.lang.String arg0)

从输出,我们可知编译器为我们添加了以下几个特性:SNr28资讯网——每日最新资讯28at.com

  1. 针对每一个定义的枚举值,添加一个同名的 public static final 的属性
  2. 添加一个private static final <pre>不能识别此Latex公式: VALUES 属性记录枚举中所有的值信息
  3. 添加一个静态的 values 方法,返回枚举中所有的值信息(</pre>VALUES)
  4. 添加一个静态的 valueOf 方法,用于通过 name 获取枚举值(调用 Enum 中的 valueOf 方法)

2.2. 修复方案

了解枚举的基础知识后,落地方案也就变的非常简单,只需:SNr28资讯网——每日最新资讯28at.com

  • 构建一个枚举类 ProductType,将所有支持的类型添加到枚举中;
  • 将原来 OrderItem 中的 productType 从原来的 Integer 替换为 ProductType;

具体代码如下:SNr28资讯网——每日最新资讯28at.com

// 将产品类型定义为 枚举public enum ProductType {    CLAZZ, BOOK; // 定义系统所支持的类型}// 领域对象中直接使用 ProductType 枚举public class OrderItem{    // 将原来的 Integer 替换为 ProductType    private ProductType productType;}// 创建单个订单的请求对象@Data@ApiModel(description = "创建单个订单")class CreateOrderRequest {    @ApiModelProperty(value = "产品类型")    private ProductType productType;    @ApiModelProperty(value = "产品id")    private Integer productId;    @ApiModelProperty(value = "数量")    private Integer amount;}

新的 Swagger 如下:SNr28资讯网——每日最新资讯28at.com

图片图片SNr28资讯网——每日最新资讯28at.com

可见,ProductType 被定义为枚举类型,并直接给出了全部备选项。SNr28资讯网——每日最新资讯28at.com

3. 更多应用场景

枚举的核心是==具有固定值的集合==,非常适用于各种类型(Type)、状态(Status) 这些场景,所以在系统中看到 Type、Status、State 等关键字时,需要慎重考虑是否可以使用枚举。SNr28资讯网——每日最新资讯28at.com

但,枚举作为一种特殊的类,也为很多场景提供了更优雅的解决方案。SNr28资讯网——每日最新资讯28at.com

3.1. Switch

在Java 1.5之前,只有一些简单类型(int,short,char,byte)可以用于 switch 的 case 语句,我们习惯采用 ‘常量+case’ 的方式增加代码的可读性,但是丢失了类型系统的校验。由于枚举的 ordinal 特性的存在,可以将其用于case语句。SNr28资讯网——每日最新资讯28at.com

public class FruitConstant {    public static final int APPLE = 1;    public static final int BANANA = 2;    public static final int PEAR = 3;}// 没有类型保障public String nameByConstant(int fruit){    switch (fruit){        case FruitConstant.APPLE:            return "苹果";        case FruitConstant.BANANA:            return "香蕉";        case FruitConstant.PEAR:            return "梨";    }    return "未知";}// 使用枚举public enum FruitEnum {    APPLE,    BANANA,    PEAR;}// 有类型保障public String nameByEnum(FruitEnum fruit){    switch (fruit){        case APPLE:            return "苹果";        case BANANA:            return "香蕉";        case PEAR:            return "梨";    }    return "未知";}

3.2. 单例

Java中单例的编写主要有饿汉式、懒汉式、静态内部类等几种方式(双重锁判断存在缺陷),但还有一种简单的方式是基于枚举的单例。SNr28资讯网——每日最新资讯28at.com

public interface Converter<S, T> {    T convert(S source);}// 每一个枚举值都是一个单例对象public enum Date2StringConverters implements Converter<Date, String>{    yyyy_MM_dd("yyyy-MM-dd"),    yyyy_MM_dd_HH_mm_ss("yyyy-MM-dd HH:mm:ss"),    HH_mm_ss("HH:mm:ss");    private final String dateFormat;    Date2StringConverters(String dateFormat) {        this.dateFormat = dateFormat;    }    @Override    public String convert(Date source) {        return new SimpleDateFormat(this.dateFormat).format(source);    }}public class ConverterTests {    private final Converter<Date, String> converter1 = Date2StringConverters.yyyy_MM_dd;    private final Converter<Date, String> converter2 = Date2StringConverters.yyyy_MM_dd_HH_mm_ss;    private final Converter<Date, String> converter3 = Date2StringConverters.HH_mm_ss;    public void formatTest(Date date){        System.out.println(converter1.convert(date));        System.out.println(converter2.convert(date));        System.out.println(converter3.convert(date));    }}

3.3. 状态机

状态机是解决业务流程中的一种有效手段,而枚举的单例性,为构建状态机提供了便利。SNr28资讯网——每日最新资讯28at.com

以下是一个订单的状态扭转流程,所涉及的状态包括 Created、Canceled、Confirmed、Overtime、Paied;所涉及的动作包括cancel、confirm、timeout、pay。SNr28资讯网——每日最新资讯28at.com

graph TBNone{开始}--> |create|CreatedCreated-->|confirm|ConfirmedCreated-->|cancel|CanceldConfirmed-->|cancel|CanceldConfirmed-->|timeout|OvertimeConfirmed-->|pay| Paied
// 状态操作接口,管理所有支持的动作public interface IOrderState {    void cancel(OrderStateContext context);    void confirm(OrderStateContext context);    void timeout(OrderStateContext context);    void pay(OrderStateContext context);}// 状态机上下文public interface OrderStateContext {    void setStats(OrderState state);}// 订单实际实现public class Order{    private OrderState state;    private void setStats(OrderState state) {        this.state = state;    }    // 将请求转发给状态机    public void cancel() {        this.state.cancel(new StateContext());    }    // 将请求转发给状态机    public void confirm() {        this.state.confirm(new StateContext());    }    // 将请求转发给状态机    public void timeout() {        this.state.timeout(new StateContext());    }    // 将请求转发给状态机    public void pay() {        this.state.pay(new StateContext());    }    // 内部类,实现OrderStateContext,回写Order的状态    class StateContext implements OrderStateContext{        @Override        public void setStats(OrderState state) {            Order.this.setStats(state);        }    }}// 基于枚举的状态机实现public enum OrderState implements IOrderState{    CREATED{        // 允许进行cancel操作,并把状态设置为CANCELD        @Override        public void cancel(OrderStateContext context){            context.setStats(CANCELD);        }        // 允许进行confirm操作,并把状态设置为CONFIRMED        @Override        public void confirm(OrderStateContext context) {            context.setStats(CONFIRMED);        }    },    CONFIRMED{        // 允许进行cancel操作,并把状态设置为CANCELD        @Override        public void cancel(OrderStateContext context) {            context.setStats(CANCELD);        }        // 允许进行timeout操作,并把状态设置为OVERTIME        @Override        public void timeout(OrderStateContext context) {            context.setStats(OVERTIME);        }        // 允许进行pay操作,并把状态设置为PAIED        @Override        public void pay(OrderStateContext context) {            context.setStats(PAIED);        }    },    // 最终状态,不允许任何操作    CANCELD{    },    // 最终状态,不允许任何操作    OVERTIME{    },    // 最终状态,不允许任何操作    PAIED{    };    @Override    public void cancel(OrderStateContext context) {        throw new NotSupportedException();    }    @Override    public void confirm(OrderStateContext context) {        throw new NotSupportedException();    }    @Override    public void timeout(OrderStateContext context) {        throw new NotSupportedException();    }    @Override    public void pay(OrderStateContext context) {        throw new NotSupportedException();    }}

3.4. 责任链

在责任链模式中,程序可以使用多种方式来处理一个问题,然后把他们链接起来,当一个请求进来后,他会遍历整个链,找到能够处理该请求的处理器并对请求进行处理。SNr28资讯网——每日最新资讯28at.com

枚举可以实现某个接口,加上其天生的单例特性,可以成为组织责任链处理器的一种方式。SNr28资讯网——每日最新资讯28at.com

// 消息类型public enum MessageType {    TEXT, BIN, XML, JSON;}// 定义的消息体@Valuepublic class Message {    private final MessageType type;    private final Object object;    public Message(MessageType type, Object object) {        this.type = type;        this.object = object;    }}// 消息处理器public interface MessageHandler {    boolean handle(Message message);}
// 基于枚举的处理器管理public enum MessageHandlers implements MessageHandler{    TEXT_HANDLER(MessageType.TEXT){        @Override        boolean doHandle(Message message) {            System.out.println("text");            return true;        }    },    BIN_HANDLER(MessageType.BIN){        @Override        boolean doHandle(Message message) {            System.out.println("bin");            return true;        }    },    XML_HANDLER(MessageType.XML){        @Override        boolean doHandle(Message message) {            System.out.println("xml");            return true;        }    },    JSON_HANDLER(MessageType.JSON){        @Override        boolean doHandle(Message message) {            System.out.println("json");            return true;        }    };    // 接受的类型    private final MessageType acceptType;    MessageHandlers(MessageType acceptType) {        this.acceptType = acceptType;    }    // 抽象接口    abstract boolean doHandle(Message message);    // 如果消息体是接受类型,调用doHandle进行业务处理    @Override    public boolean handle(Message message) {        return message.getType() == this.acceptType && doHandle(message);    }}
// 消息处理链public class MessageHandlerChain {    public boolean handle(Message message){        for (MessageHandler handler : MessageHandlers.values()){            if (handler.handle(message)){                return true;            }        }        return false;    }}

3.5. 分发器

分发器根据输入的数据,找到对应的处理器,并将请求转发给处理器进行处理。 由于 EnumMap 其出色的性能,特别适合根据特定类型作为分发策略的场景。SNr28资讯网——每日最新资讯28at.com

// 消息体@Valuepublic class Message {    private final MessageType type;    private final Object data;    public Message(MessageType type, Object data) {        this.type = type;        this.data = data;    }}// 消息类型public enum MessageType {    // 登录    LOGIN,    // 进入房间    ENTER_ROOM,    // 退出房间    EXIT_ROOM,    // 登出    LOGOUT;}// 消息处理器public interface MessageHandler {    void handle(Message message);}
// 基于EnumMap的消息分发器public class MessageDispatcher {    private final Map<MessageType, MessageHandler> dispatcherMap =             new EnumMap<MessageType, MessageHandler>(MessageType.class);    public MessageDispatcher(){        dispatcherMap.put(MessageType.LOGIN, message -> System.out.println("Login"));        dispatcherMap.put(MessageType.ENTER_ROOM, message -> System.out.println("Enter Room"));        dispatcherMap.put(MessageType.EXIT_ROOM, message -> System.out.println("Exit Room"));        dispatcherMap.put(MessageType.LOGOUT, message -> System.out.println("Logout"));    }    public void dispatch(Message message){        MessageHandler handler = this.dispatcherMap.get(message.getType());        if (handler != null){            handler.handle(message);        }    }}

4. 示例&源码

仓库地址:https://gitee.com/litao851025/learnFromBug/SNr28资讯网——每日最新资讯28at.com

代码地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/limitSNr28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-41674-0.html【故障现场】控制好取值范围,甭给别人犯错的机会

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 昇腾 AI 开发者创享日・广州站成功举办 四大仪式激发人工智能产业创新活力

下一篇: 聊聊分布式数据库TDSQL的技术架构

标签:
  • 热门焦点
Top