# 1. 问题&分析

## 1.1. 案例

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

1. 小艾认为核心问题是调用方没有按规范进行传参,所以主要责任在大飞哥;

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

两人各持己见争论不休,你认为责任在谁呢?

## 1.2. 问题分析

在订单系统中,商品类型定义为 Integer 类型,使用静态常量来表示系统所支持的具体值,核心代码如下:

```java

public class OrderItem{

private Integer productType;

}

public class ProductTypes{

public static final Integer CLAZZ = 1;

public static final Integer BOOK = 2;

....

}

```

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

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/book-life/enums/product_int.png)

商品类型(ProductType)在系统中是一个字典,有自己的固定取值范围,定义为 Integer 将放大可接受的值,一旦值在 ProductType 之外便会发生系统异常。

# 2. 解决方案

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

将 ProductType 可接受的取值范围与类型的取值范围保存一致!!!

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/book-life/enums/product_type.png)

这正是枚举重要的应用场景。

> 【原则】规范、流程 在没有检测机制相辅助时都不可靠。如有可能,请使用编译器进行强制约束!!!

## 2.1. 枚举基础知识

> 关键词 enum 可以将一组具名值的有限集合创建成一种新的类型,而这些具名的值可以作为常规程序组件使用。

枚举最常见的用途便是替换常量定义,为其增添类型约束,完成编译时类型验证。

### 2.1.1 枚举定义

> 枚举的定义与类和常量定义非常类似。使用 enum 关键字替换 class 关键字,然后在 enum 中定义“常量”即可。

对于 ProductType 枚举方案如下:

```java

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,即为定义的顺序。

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/book-life/enums/enum.png)

简单测试如下:

```java

@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);

}

}

```

输出结果为:

```java

ProductType:CLAZZ

true

ProductType:BOOK

true

```

ordrial测试如下:

```java

@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);

}

}

```

输出结果如下:

```java

ProductType:0

true

ProductType:1

true

```

从输出上可以清晰的看出:

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

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

### 2.1.4. 枚举的本质

> enum可以理解为编译器的语法糖,在创建 enum 时,编译器会为你生成一个相关的类,这个类继承自 java.lang.Enum。

先看下Enum提供了什么:

```java

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 中我们可以得到:

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,又是从怎么来的呢?这些是编译器为其添加的。

```java

@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 中的属性和方法进行打印,结果如下:

```java

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)

```

从输出,我们可知编译器为我们添加了以下几个特性:

1. 针对每一个定义的枚举值,添加一个同名的 public static final 的属性

2. 添加一个private static final \$VALUES 属性记录枚举中所有的值信息

3. 添加一个静态的 values 方法,返回枚举中所有的值信息(\$VALUES)

4. 添加一个静态的 valueOf 方法,用于通过 name 获取枚举值(调用 Enum 中的 valueOf 方法)

## 2.2. 枚举是一个特殊类

> 虽然编译器为枚举添加了很多功能,但究其本质,枚举终究是一个类。除了必须继承自 Enum 外,我们基本上可以将 enum 看成一个常规类,因此属性、方法、接口等在枚举中仍旧有效。

### 2.2.1. 枚举中的属性和方法

> 除了编译器为我们添加的方法外,我们也可以在枚举中添加新的属性和方法,甚至可以有main方法。

```java

public enum ProductType {

CLAZZ("课程"),

BOOK("图书");

private final String descr;

private ProductType(String descr) {

this.descr = descr;

}

public String getDescr(){

return this.descr;

}

public static void main(String... args){

for (ProductType productType : ProductType.values()){

System.out.println(productType.toString() + ":" + productType.getDescr());

}

}

}

```

main执行输出结果:

```java

CLAZZ:课程

BOOK:图书

```

如果准备添加自定义方法,需要在 enum 实例序列的最后添加一个分号。同时 java 要求必须先定义 enum 实例,如果在定义 enum 实例前定义任何属性和方法,那么在编译过程中会得到相应的错误信息。

enum 中的构造函数和普通类没有太多的区别,但由于只能在 enum 中使用构造函数,其默认为 private,如果尝试升级可见范围,编译器会给出相应错误信息。

### 2.2.2. 重写枚举方法

> 枚举中的方法与普通类中方法并无差别,可以对其进行重写。其中 Enum 类中的 name 和 ordrial 两个方法为final,无法重写。

```java

public enum ProductType {

CLAZZ("课程"),

BOOK("图书");

private final String descr;

ProductType(String descr) {

this.descr = descr;

}

@Override

public String toString(){

return this.descr;

}

public static void main(String... args){

for (ProductType productType : ProductType.values()){

System.out.println(productType.name() + ":" + userStatus.toString());

}

}

}

```

main输出结果为

```java

CLAZZ:课程

BOOK:图书

```

重写toString方法,返回描述信息。

### 2.2.3. 实现接口

> 由于所有的 enum 都继承自 java.lang.Enum 类,而 Java 不支持多继承,所以我们的 enum 不能再继承其他类型,但 enum 可以同时实现一个或多个接口,从而对其进行扩展。

```java

public interface CodeBasedEnum {

int code();

}

public enum CodeBasedProductType implements CodeBasedEnum{

CLAZZ(1), BOOK(2);

private final int code;

CodeBasedProductType(int code) {

this.code = code;

}

@Override

public int code(){

return this.code;

}

public static void main(String... arg){

for (CodeBasedProductType codeBasedEnum : CodeBasedProductType.values()){

System.out.println(codeBasedEnum.name() + ":" + codeBasedEnum.code());

}

}

}

```

main函数输出结果:

```java

CLAZZ:1

BOOK:2

```

## 2.3. 改进方案

了解枚举的基础知识后,落地方案也就变的非常简单,只需:

1. 构建一个枚举类 ProductType,将所有支持的类型添加到枚举中;

2. 将原来 OrderItem 中的 productType 从原来的 Integer 替换为 ProductType;

具体代码如下:

```java

public enum ProductType {

CLAZZ, BOOK; // 定义系统所支持的类型

}

public class OrderItem{

// 将原来的 Integer 替换为 ProductType

private ProductType productType;

}

```

在对核心模型完成替换后,需对相关类进行替换,比如:

1. 入参及验证。

2. 持久化实体。

3. 视图模型。

做完以上工作后,就在也不用担心由于系统接收非法值而产生问题。

# 3. 更多应用场景

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

但,枚举作为一种特殊的类,也为很多场景提供了更优雅的解决方案。

## 3.1. Switch

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

```java

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中单例的编写主要有饿汉式、懒汉式、静态内部类等几种方式(双重锁判断存在缺陷),但还有一种简单的方式是基于枚举的单例。

```java

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. 状态机

> 状态机是解决业务流程中的一种有效手段,而枚举的单例性,为构建状态机提供了便利。

以下是一个订单的状态扭转流程,所涉及的状态包括 Created、Canceled、Confirmed、Overtime、Paied;所涉及的动作包括cancel、confirm、timeout、pay。

```mermaid

graph TB

None{开始}--> |create|Created

Created-->|confirm|Confirmed

Created-->|cancel|Canceld

Confirmed-->|cancel|Canceld

Confirmed-->|timeout|Overtime

Confirmed-->|pay| Paied

```

```java

// 状态操作接口,管理所有支持的动作

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. 责任链

> 在责任链模式中,程序可以使用多种方式来处理一个问题,然后把他们链接起来,当一个请求进来后,他会遍历整个链,找到能够处理该请求的处理器并对请求进行处理。

枚举可以实现某个接口,加上其天生的单例特性,可以成为组织责任链处理器的一种方式。

```java

// 消息类型

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

}

```

```java

// 基于枚举的处理器管理

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

}

}

```

```java

// 消息处理链

public class MessageHandlerChain {

public boolean handle(Message message){

for (MessageHandler handler : MessageHandlers.values()){

if (handler.handle(message)){

return true;

}

}

return false;

}

}

```

## 3.5. 分发器

> 分发器根据输入的数据,找到对应的处理器,并将请求转发给处理器进行处理。 由于 EnumMap 其出色的性能,特别适合根据特定类型作为分发策略的场景。

```java

// 消息体

@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);

}

```

```java

// 基于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. 避坑指南

枚举本身非常简单,但一不小心还是会陷入各种坑。

## 4.1. 枚举引入的风险

枚举最大的问题便是,仅有的 name 和 ordrial 无法满足多样的应用需求。比如:

1. 没有统一的唯一性标识

2. 没有统一的展示描述

### 4.1.1. 重构是最大风险

枚举最大的坑在于:==需要谨慎处理变更。==

枚举可以使用 name 或 ordrial 来表示,但两者对重构都不太友好,对枚举的调整将直接对两者产生巨大的破坏力。

例如,原订单状态中只有一个“取消”状态(CANCELLED)当订单超时未支付时,系统自动将状态变为 “取消”,随着业务发展 需要添加 “手工取消” 功能,这时就需要对原来的 “取消”状态(CANCELLED) 重命名 为 “超时取消”(TIMEOUT\_CANCELLED)并新增状态 “手工取消” (MANUAL CANCELLED)。

代码如下:

```java

public enum OrderStatus{

CREATED, // 已创建

CANCELLED, // 已取消

PAID, // 已支付

FINISHED // 已完成

}

调整之后的枚举为:

public enum OrderStatus{

CREATED, // 已创建

TIMEOUT_CANCELLED, // 超时自动取消

MANUAL_CANCELLED, // 手工需求

PAID, // 已支付

FINISHED // 已完成

}

```

整个重构对领域层冲击很小,但对调用方、存储产生很大影响,重构后:

1. CANCELLED 更新为 TIMEOUT\_CANCELLED,前端和数据库都需要进行调整,如有遗漏便是线上问题;

2. 枚举定义顺序发生变化,前端和数据库也需要同步调整;

可见,枚举中的 name 和 ordrial 都无法非常好的应对变化,我们需要为它创建一个新的 “唯一标识”

> 当然,你也可以定义规范:

>

> 1. 枚举不能进行 Rename 操作;

> 2. 新增枚举必须放在最后;

>

> 请谨记原则:规范是一种“无能”的表现。只会为系统埋下巨大的“雷”,然后迎来后来人的“粉身碎骨”。

### 4.1.2. 统一展示也是一种挑战

枚举中的 name 和 ordrial 都不适合直接展示给用户,往往由前端负责转换。主要涉及以下几点:

1. 下拉选择框,需要将展示的信息转换为后端的“唯一标识”,比如将“超时取消” 转换为 “TIMEOUT\_CANCELLED”

2. 数据展示,需要将枚举转换为具体的描述,比如 TIMEOUT\_CANCELLED 转换为 “超时取消”;

由于这两者本来就是一回事,在系统设计中需要进行统一管理。由于前端的特点,很难找到统一维护处,往往分散在不同 js 中,每次更新都是惨无人道的全局查找并修改,一旦丢失就有是线上问题。

## 4.2. 解决方案

### 4.2.1. 规范化枚举

为了更好的解决上面两个副作用,需要为枚举添加新的能力:

1. 添加 getCode() 用作枚举的唯一标识

2. 添加 getDescription() 用于控制枚举的展示

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/book-life/enums/enum_ext.png)

由于枚举可以实现接口,可以通过定义接口来规范枚举的行为,具体代码如下:

```java

// 添加唯一标识Code

public interface CodeBasedEnum {

int getCode();

}

// 添加描述信息 description

public interface SelfDescribedEnum {

default String getName(){

return name();

}

String name();

String getDescription();

}

// 定义统一的枚举接口

public interface CommonEnum extends CodeBasedEnum, SelfDescribedEnum{

}

```

整体结构如下:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/commonenum.png "image")

在定义枚举时便可以直接实现 CommonEnum 这个接口。示例如下:

```java

public enum NewsStatus implements CommonEnum {

DELETE(1, "删除"),

ONLINE(10, "上线"),

OFFLINE(20, "下线");

private final int code;

private final String desc;

NewsStatus(int code, String desc) {

this.code = code;

this.desc = desc;

}

@Override

public int getCode() {

return this.code;

}

@Override

public String getDescription() {

return this.desc;

}

}

```

### 4.2.2. 统一管理 CommonEnum

统一 CommonEnum 最大的好处便是可以进行统一管理,对于统一管理,第一件事便是找到并注册所有的 CommonEnum。

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/registry.png "image")

以上是核心处理流程:

1. 首先通过 Spring 的 ResourcePatternResolver 根据配置的 basePackage 对 classpath 进行扫描

2. 扫描结果以 Resource 来表示,通过 MetadataReader 读取 Resource 信息,并将其解析为 ClassMetadata

3. 获得 ClassMetadata 之后,找出实现 CommonEnum 的类

4. 将 CommonEnum 实现类注册到两个 Map 中进行缓存

> 备注:此处万万不可直接使用反射技术,反射会触发类的自动加载,将对众多不需要的类进行加载,从而增加 metaspace 的压力。

在需要 CommonEnum 时,只需注入 CommonEnumRegistry Bean 便可以方便的获得 CommonEnum 的具体实现。

### 4.2.3. Spring MVC 集成

有了统一的 CommonEnum,可以对 Spring MVC 进行功能加强,主要有以下几个功能点:

1. 输入参数使用 code 作为唯一标识,避免 name、ordrial 变化导致业务异常。

2. 对返回值进行功能加强,展示信息应该包括枚举的 code、name、description 等信息。

整体架构如下:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/springmvc2.png)

#### 4.2.3.1. 入参集成

> 核心就是以 code 作为枚举的唯一标识,自动完成 code 到枚举的转化。

Spring MVC 存在两种参数转化扩展:

1. 对于普通参数,比如 RequestParam 或 PathVariable 直接从 ConditionalGenericConverter 进行扩展

1. 基于 CommonEnumRegistry 提供的 CommonEnum 信息,对 matches 和 getConvertibleTypes方法进行重写

2. 根据目标类型获取所有的 枚举值,并根据 code 和 name 进行转化

2. 对于 Json 参数,需要对 Json 框架进行扩展(以 Jackson 为例)

1. 遍历 CommonEnumRegistry 提供的所有 CommonEnum,依次进行注册

2. 从 Json 中读取信息,根据 code 和 name 转化为确定的枚举值

两种扩展核心实现见:

```java

@Order(1)

@Component

public class CommonEnumConverter implements ConditionalGenericConverter {

@Autowired

private CommonEnumRegistry enumRegistry;

@Override

public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {

Class<?> type = targetType.getType();

return enumRegistry.getClassDict().containsKey(type);

}

@Override

public Set<ConvertiblePair> getConvertibleTypes() {

return enumRegistry.getClassDict().keySet().stream()

.map(cls -> new ConvertiblePair(String.class, cls))

.collect(Collectors.toSet());

}

@Override

public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {

String value = (String) source;

List<CommonEnum> commonEnums = this.enumRegistry.getClassDict().get(targetType.getType());

return commonEnums.stream()

.filter(commonEnum -> commonEnum.match(value))

.findFirst()

.orElse(null);

}

}

static class CommonEnumJsonDeserializer extends JsonDeserializer{

private final List<CommonEnum> commonEnums;

CommonEnumJsonDeserializer(List<CommonEnum> commonEnums) {

this.commonEnums = commonEnums;

}

@Override

public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {

String value = jsonParser.readValueAs(String.class);

return commonEnums.stream()

.filter(commonEnum -> commonEnum.match(value))

.findFirst()

.orElse(null);

}

}

```

#### 4.2.3.2. 返回集成

默认情况下,对于枚举类型在转换为 Json 时,只会输出 name,其他信息会出现丢失,对于展示非常不友好,对此,需要对 Json 序列化进行能力增强。

首先,需要定义 CommonEnum 对应的返回对象,具体如下:

```java

@Value

@AllArgsConstructor(access = AccessLevel.PRIVATE)

@ApiModel(description = "通用枚举")

public class CommonEnumVO {

@ApiModelProperty(notes = "Code")

private final int code;

@ApiModelProperty(notes = "Name")

private final String name;

@ApiModelProperty(notes = "描述")

private final String desc;

public static CommonEnumVO from(CommonEnum commonEnum){

if (commonEnum == null){

return null;

}

return new CommonEnumVO(commonEnum.getCode(), commonEnum.getName(), commonEnum.getDescription());

}

public static List<CommonEnumVO> from(List<CommonEnum> commonEnums){

if (CollectionUtils.isEmpty(commonEnums)){

return Collections.emptyList();

}

return commonEnums.stream()

.filter(Objects::nonNull)

.map(CommonEnumVO::from)

.filter(Objects::nonNull)

.collect(Collectors.toList());

}

}

```

CommonEnumVO 是一个标准的 POJO,只是增加了 Swagger 相关注解。

CommonEnumJsonSerializer 是自定义序列化的核心,会将 CommonEnum 封装为 CommonEnumVO 并进行写回,具体如下:

```java

static class CommonEnumJsonSerializer extends JsonSerializer{

@Override

public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {

CommonEnum commonEnum = (CommonEnum) o;

CommonEnumVO commonEnumVO = CommonEnumVO.from(commonEnum);

jsonGenerator.writeObject(commonEnumVO);

}

}

```

#### 4.2.3.3. 效果展示

首先,新建一个测试枚举 NewsStatus,具体如下:

```java

public enum NewsStatus implements CommonEnum {

DELETE(1, "删除"),

ONLINE(10, "上线"),

OFFLINE(20, "下线");

private final int code;

private final String desc;

NewsStatus(int code, String desc) {

this.code = code;

this.desc = desc;

}

@Override

public int getCode() {

return this.code;

}

@Override

public String getDescription() {

return this.desc;

}

}

```

然后新建 EnumController,具体如下:

```java

@RestController

@RequestMapping("enum")

public class EnumController {

@GetMapping("paramToEnum")

public RestResult<CommonEnumVO> paramToEnum(@RequestParam("newsStatus") NewsStatus newsStatus){

return RestResult.success(CommonEnumVO.from(newsStatus));

}

@GetMapping("pathToEnum/{newsStatus}")

public RestResult<CommonEnumVO> pathToEnum(@PathVariable("newsStatus") NewsStatus newsStatus){

return RestResult.success(CommonEnumVO.from(newsStatus));

}

@PostMapping("jsonToEnum")

public RestResult<CommonEnumVO> jsonToEnum(@RequestBody NewsStatusRequestBody newsStatusRequestBody){

return RestResult.success(CommonEnumVO.from(newsStatusRequestBody.getNewsStatus()));

}

@GetMapping("bodyToJson")

public RestResult<NewsStatusResponseBody> bodyToJson(){

NewsStatusResponseBody newsStatusResponseBody = new NewsStatusResponseBody();

newsStatusResponseBody.setNewsStatus(Arrays.asList(NewsStatus.values()));

return RestResult.success(newsStatusResponseBody);

}

@Data

public static class NewsStatusRequestBody {

private NewsStatus newsStatus;

}

@Data

public static class NewsStatusResponseBody {

private List<NewsStatus> newsStatus;

}

}

```

执行结果如下:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/mvc_result.png "image")

整体符合预期:

1. 使用 code 作为请求参数可以自动转化为对应的 CommonEnum

2. 使用 CommonEnum 作为返回值,返回标准的 CommonEnumVO 对象结构

### 4.2.4. 通用枚举字典

有时可以将 枚举 理解为系统的一类字典,比较典型的就是管理页面的各种下拉框,下拉框中的数据来自于后台服务。

有了 CommonEnum 之后,可以提供统一的枚举字典接口,避免重复开发,同时在新增枚举时也无需编码,系统自动识别并添加到字典中。

#### 4.2.4.1. 构建字典Controller

在 CommonEnumRegistry 基础之上实现通用字典接口非常简单,只需按规范构建 Controller 即可,具体如下:

```java

@Api(tags = "通用字典接口")

@RestController

@RequestMapping("/enumDict")

@Slf4j

public class EnumDictController {

@Autowired

private CommonEnumRegistry commonEnumRegistry;

@GetMapping("all")

public RestResult<Map<String, List<CommonEnumVO>>> allEnums(){

Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict();

Map<String, List<CommonEnumVO>> dictVo = Maps.newHashMapWithExpectedSize(dict.size());

for (Map.Entry<String, List<CommonEnum>> entry : dict.entrySet()){

dictVo.put(entry.getKey(), CommonEnumVO.from(entry.getValue()));

}

return RestResult.success(dictVo);

}

@GetMapping("types")

public RestResult<List<String>> enumTypes(){

Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict();

return RestResult.success(Lists.newArrayList(dict.keySet()));

}

@GetMapping("/{type}")

public RestResult<List<CommonEnumVO>> dictByType(@PathVariable("type") String type){

Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict();

List<CommonEnum> commonEnums = dict.get(type);

return RestResult.success(CommonEnumVO.from(commonEnums));

}

}

```

该 Controller 提供如下能力:

1. 获取全部字典,一次性获取系统中所有的 CommonEnum

2. 获取所有字典类型,仅获取字典类型,通常用于测试

3. 获取指定字典类型的全部信息,比如上述所说的填充下拉框

#### 4.2.4.2. 效果展示

获取全部字典:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/all_dict.png "image")

获取所有字典类型:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/types_dict.png "image")

获取指定字段类型的全部信息:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/news_status_dict.png "image")

### 4.2.5. 存储层集成

> 由于 code 是枚举的唯一标识,在数据存储时也需要完成 code 与 枚举 间的双向转换。

应用程序与存储引擎间主要由各类 ORM 框架完成通讯,这些 ORM 框架均提供了类型映射的扩展点,通过该扩展点可以完成 code 与 CommonEnum 的双向转换。

#### 4.2.5.1. MyBatis 集成

MyBatis 作为最流行的 ORM 框架,提供了 TypeHandler 用于处理自定义的类型扩展。

```java

@MappedTypes(NewsStatus.class)

public class MyBatisNewsStatusHandler extends CommonEnumTypeHandler<NewsStatus> {

public MyBatisNewsStatusHandler() {

super(NewsStatus.values());

}

}

```

MyBatisNewsStatusHandler 通过 @MappedTypes(NewsStatus.class) 对其进行标记,以告知框架该 Handler 是用于 NewsStatus 类型的转换。

CommonEnumTypeHandler 是为 CommonEnum 提供的通用转化能力,具体如下:

```java

public abstract class CommonEnumTypeHandler<T extends Enum<T> & CommonEnum>

extends BaseTypeHandler<T> {

private final List<T> commonEnums;

protected CommonEnumTypeHandler(T[] commonEnums){

this(Arrays.asList(commonEnums));

}

protected CommonEnumTypeHandler(List<T> commonEnums) {

this.commonEnums = commonEnums;

}

@Override

public void setNonNullParameter(PreparedStatement preparedStatement, int i, T t, JdbcType jdbcType) throws SQLException {

preparedStatement.setInt(i, t.getCode());

}

@Override

public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException {

int code = resultSet.getInt(columnName);

return commonEnums.stream()

.filter(commonEnum -> commonEnum.match(String.valueOf(code)))

.findFirst()

.orElse(null);

}

@Override

public T getNullableResult(ResultSet resultSet, int i) throws SQLException {

int code = resultSet.getInt(i);

return commonEnums.stream()

.filter(commonEnum -> commonEnum.match(String.valueOf(code)))

.findFirst()

.orElse(null);

}

@Override

public T getNullableResult(CallableStatement callableStatement, int i) throws SQLException {

int code = callableStatement.getInt(i);

return commonEnums.stream()

.filter(commonEnum -> commonEnum.match(String.valueOf(code)))

.findFirst()

.orElse(null);

}

}

```

由于逻辑比较简单,在此不做过多解释。

有了类型之后,需要在 spring boot 的配置文件中指定 type-handler 的加载逻辑,具体如下:

mybatis:

type-handlers-package: com.geekhalo.lego.enums.mybatis

完成配置后,使用 Mapper 对数据进行持久化,数据表中存储的便是 code 信息,具体如下:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/mybatis_code_table.png "image")

#### 4.2.5.1. JPA 集成

随着 Spring data 越来越流行,JPA 又焕发出新的活力,JPA 提供 AttributeConverter 以对属性转换进行自定义。

首先,构建 JpaNewsStatusConverter,具体如下:

```java

public class JpaNewsStatusConverter extends CommonEnumAttributeConverter<NewsStatus> {

public JpaNewsStatusConverter() {

super(NewsStatus.values());

}

}

```

CommonEnumAttributeConverter 为 CommonEnum 提供的通用转化能力,具体如下:

```java

public abstract class CommonEnumAttributeConverter<E extends Enum<E> & CommonEnum>

implements AttributeConverter<E, Integer> {

private final List<E> commonEnums;

public CommonEnumAttributeConverter(E[] commonEnums){

this(Arrays.asList(commonEnums));

}

public CommonEnumAttributeConverter(List<E> commonEnums) {

this.commonEnums = commonEnums;

}

@Override

public Integer convertToDatabaseColumn(E e) {

return e.getCode();

}

@Override

public E convertToEntityAttribute(Integer code) {

return (E) commonEnums.stream()

.filter(commonEnum -> commonEnum.match(String.valueOf(code)))

.findFirst()

.orElse(null);

}

}

```

在有了 JpaNewsStatusConverter 之后,我们需要在 Entity 的属性上增加配置信息,具体如下:

```java

@Entity

@Data

@Table(name = "t_jpa_news")

public class JpaNewsEntity {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

@Convert(converter = JpaNewsStatusConverter.class)

private NewsStatus status;

}

```

@Convert(converter = JpaNewsStatusConverter.class) 是对 status 的配置,使用 JpaNewsStatusConverter 进行属性的转换。

运行持久化指令后,数据库如下:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/lego/leetcode/enum/jpa_code_table.png "image")

# 5. 小结

文章篇幅有些长,整体结构如下:

![image](https://geekhalo-lego.oss-cn-beijing.aliyuncs.com/book-life/enums/top_view.png)

整体内容包括:

1. 从一个线上问题出发,productType 被定义为 Integer 类型,导致接收到超出系统正常值的入参,最终触发线上bug

2. 这是典型的 类型取值范围 大于 系统接受范围 的场景,由于没有非常好的边界保护,非常容易造成溢出问题

3. 使用 枚举 对类型的取值范围进行限制,从根源上杜绝该类问题的出现

4. 给出基于枚举的最佳实践,包括 枚举定义 和 枚举使用

5. 给出枚举其他的应用场景,包括 switch、单例、状态机、责任链、分发器等

6. 最后,给出枚举使用中的那些坑,主要为枚举增加 code 作为唯一标识,增加 description 作为统一展示,然后与常见框架进行集中,包括

1. 统一的枚举管理

2. Spring MVC 集成

3. 统一字典 API

4. 存储层集成