1. 问题&分析
1.1. 案例
小艾刚刚和大飞哥炒了一架,心情非常低落。整个事情是这样,小艾前段时间刚刚接手订单系统,今天收到一大波线上 NPE (Null Pointer Exception)报警,经排查发现订单表的商品类型(ProductType)出现一组非法值,在展示订单时由于系统无法识别这些非法值导致空指针异常。小艾通过排查,发现订单来自于市场团队,于是找到团队负责人大飞哥,并把现状和排查结果进行同步。经过大飞哥的排查,确实是在前端的各种跳转过程中导致 商品类型参数 被覆盖,立即安排紧急上线进行修复。整个事情处理速度快也没造成太大损失,但在事故复盘过程中出现了偏差:
两人各持己见争论不休,你认为责任在谁呢?
1.2. 问题分析
在订单系统中,商品类型定义为 Integer 类型,使用静态常量来表示系统所支持的具体值,核心代码如下:
// 领域对象
public class OrderItem{
private Integer productType;
}
// 定义 ProductTypes 管理所有支持的 ProductType
public 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 如下:
图片
由于类型定义为 Integer, 所以当输入非法值(ProductTypes 定义之外的值)时,系统仍旧能接受并执行后续流程,这就是最核心的问题所在,如下图所示:
图片
==商品类型(ProductType)在系统中是一个字典,有自己的固定取值范围==,定义为 Integer 将放大可接受的值,一旦值在 ProductType 之外便会发生系统异常。
2. 解决方案
针对这个案例,小艾可以基于 ProductTypes 中定义的常量对所有入参进行校验,并在接入文档中进行强调。但,随着系统的发展肯定会加入更多的流程,在新流程中产生遗漏就又会出现同样的问题,那终极解决方案是什么?
将 ProductType 可接受的取值范围与类型的取值范围保存一致!!!
图片
这正是枚举重要的应用场景。
【原则】规范、流程 在没有检测机制相辅助时都不可靠。如有可能,请使用编译器进行强制约束!!!
2.1. 枚举基础知识
关键词 enum 可以将一组具名值的有限集合创建成一种新的类型,而这些具名的值可以作为常规程序组件使用。
枚举最常见的用途便是==替换常量定义==,为其增添类型约束,完成编译时类型验证。
2.1.1 枚举定义
枚举的定义与类和常量定义非常类似。使用 enum 关键字替换 class 关键字,然后在 enum 中定义“常量”即可。
对于 ProductType 枚举方案如下:
// 定义
public enum ProductType {
CLAZZ, BOOK;
}
public class OrderItem{
private ProductType productType;
}
getProductType 和 setProductType 所需类型为 ProductType,不在是比较宽泛的 Integer。在使用的时候可以通过 ProductType.XXX 的方式获取对应的枚举值,这样对类型有了更强的限制。
2.1.2. 枚举的单例性
枚举值具有单例性,及枚举中的每个值都是一个单例对象,可以直接使用 == 进行等值判断。
枚举是定义单例对象最简单的方法。
2.1.3. name 和 ordrial
对于简单的枚举,存在两个维度,一个是name,即为定义的名称;一个是ordinal,即为定义的顺序。
图片
简单测试如下:
@Test
public 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);
}
}
输出结果为:
ProductType:CLAZZ
true
ProductType:BOOK
true
ordrial测试如下:
@Test
public 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);
}
}
输出结果如下:
ProductType:0
true
ProductType:1
true
从输出上可以清晰的看出:
2.1.4. 枚举的本质
enum可以理解为编译器的语法糖,在创建 enum 时,编译器会为你生成一个相关的类,这个类继承自 java.lang.Enum。
先看下Enum提供了什么:
public abstract class Enum
implements Comparable, 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 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 getDeclaringClass() {
Class clazz = getClass();
Class zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class)clazz : (Class)zuper;
}
/**
* 静态方法,根据name获取枚举值
* @since 1.5
*/
public static T valueOf(Class 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 中我们可以得到:
到此已经解释了枚举类的大多数问题,ProductType.values(), ProductType.CLAZZ, ProductType.BOOK,又是从怎么来的呢?这些是编译器为其添加的。
@Test
public 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 中的属性和方法进行打印,结果如下:
Fields
public static final class com.example.enumdemo.ProductType CLAZZ
public static final class com.example.enumdemo.ProductType BOOK
private static final class [Lcom.example.enumdemo.ProductType; $VALUES
Methods
public staticclass [Lcom.example.enumdemo.ProductType; values()
public staticclass com.example.enumdemo.ProductType valueOf(class java.lang.String arg0)
从输出,我们可知编译器为我们添加了以下几个特性:
不能识别此Latex公式: VALUES 属性记录枚举中所有的值信息
VALUES)
2.2. 修复方案
了解枚举的基础知识后,落地方案也就变的非常简单,只需:
- 构建一个枚举类 ProductType,将所有支持的类型添加到枚举中;
- 将原来 OrderItem 中的 productType 从原来的 Integer 替换为 ProductType;
具体代码如下:
// 将产品类型定义为 枚举
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 如下:
图片
可见,ProductType 被定义为枚举类型,并直接给出了全部备选项。
3. 更多应用场景
枚举的核心是==具有固定值的集合==,非常适用于各种类型(Type)、状态(Status) 这些场景,所以在系统中看到 Type、Status、State 等关键字时,需要慎重考虑是否可以使用枚举。
但,枚举作为一种特殊的类,也为很多场景提供了更优雅的解决方案。
3.1. Switch
在Java 1.5之前,只有一些简单类型(int,short,char,byte)可以用于 switch 的 case 语句,我们习惯采用 ‘常量+case’ 的方式增加代码的可读性,但是丢失了类型系统的校验。由于枚举的 ordinal 特性的存在,可以将其用于case语句。
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中单例的编写主要有饿汉式、懒汉式、静态内部类等几种方式(双重锁判断存在缺陷),但还有一种简单的方式是基于枚举的单例。
public interface Converter {
T convert(S source);
}
// 每一个枚举值都是一个单例对象
public enum Date2StringConverters implements Converter{
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 converter1 = Date2StringConverters.yyyy_MM_dd;
private final Converter converter2 = Date2StringConverters.yyyy_MM_dd_HH_mm_ss;
private final Converter 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. 状态机
状态机是解决业务流程中的一种有效手段,而枚举的单例性,为构建状态机提供了便利。
以下是一个订单的状态扭转流程,所涉及的状态包括 Created、Canceled、Confirmed、Overtime、Paied;所涉及的动作包括cancel、confirm、timeout、pay。
graph TB
None{开始}--> |create|Created
Created-->|confirm|Confirmed
Created-->|cancel|Canceld
Confirmed-->|cancel|Canceld
Confirmed-->|timeout|Overtime
Confirmed-->|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. 责任链
在责任链模式中,程序可以使用多种方式来处理一个问题,然后把他们链接起来,当一个请求进来后,他会遍历整个链,找到能够处理该请求的处理器并对请求进行处理。
枚举可以实现某个接口,加上其天生的单例特性,可以成为组织责任链处理器的一种方式。
// 消息类型
public enum MessageType {
TEXT, BIN, XML, JSON;
}
// 定义的消息体
@Value
public 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 其出色的性能,特别适合根据特定类型作为分发策略的场景。
// 消息体
@Value
public 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 dispatcherMap =
new EnumMap(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/
代码地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/limit