故障現(xiàn)場(chǎng) | 控制好取值范圍,甭給別人犯錯(cuò)的機(jī)會(huì)
1. 問(wèn)題&分析
1.1. 案例
小艾剛剛和大飛哥炒了一架,心情非常低落。整個(gè)事情是這樣,小艾前段時(shí)間剛剛接手訂單系統(tǒng),今天收到一大波線上 NPE (Null Pointer Exception)報(bào)警,經(jīng)排查發(fā)現(xiàn)訂單表的商品類型(ProductType)出現(xiàn)一組非法值,在展示訂單時(shí)由于系統(tǒng)無(wú)法識(shí)別這些非法值導(dǎo)致空指針異常。小艾通過(guò)排查,發(fā)現(xiàn)訂單來(lái)自于市場(chǎng)團(tuán)隊(duì),于是找到團(tuán)隊(duì)負(fù)責(zé)人大飛哥,并把現(xiàn)狀和排查結(jié)果進(jìn)行同步。經(jīng)過(guò)大飛哥的排查,確實(shí)是在前端的各種跳轉(zhuǎn)過(guò)程中導(dǎo)致 商品類型參數(shù) 被覆蓋,立即安排緊急上線進(jìn)行修復(fù)。整個(gè)事情處理速度快也沒(méi)造成太大損失,但在事故復(fù)盤過(guò)程中出現(xiàn)了偏差:
- 小艾認(rèn)為核心問(wèn)題是調(diào)用方?jīng)]有按規(guī)范進(jìn)行傳參,所以主要責(zé)任在大飛哥;
- 大飛哥則認(rèn)為是訂單系統(tǒng)未對(duì)輸入?yún)?shù)進(jìn)行有效性校驗(yàn),致使問(wèn)題數(shù)據(jù)存儲(chǔ)至數(shù)據(jù)庫(kù),才出現(xiàn)后續(xù)的各種問(wèn)題,所以主要責(zé)任在小艾;
兩人各持己見(jiàn)爭(zhēng)論不休,你認(rèn)為責(zé)任在誰(shuí)呢?
1.2. 問(wèn)題分析
在訂單系統(tǒng)中,商品類型定義為 Integer 類型,使用靜態(tài)常量來(lái)表示系統(tǒng)所支持的具體值,核心代碼如下:
// 領(lǐng)域?qū)ο?public class OrderItem{
private Integer productType;
}
// 定義 ProductTypes 管理所有支持的 ProductType
public class ProductTypes{
public static final Integer CLAZZ = 1;
public static final Integer BOOK = 2;
// 其他類型
}
// 創(chuàng)建訂單的請(qǐng)求對(duì)象
@Data
@ApiModel(description = "創(chuàng)建單個(gè)訂單")
class CreateOrderRequest {
@ApiModelProperty(value = "產(chǎn)品類型")
private Integer productType;
@ApiModelProperty(value = "產(chǎn)品id")
private Integer productId;
@ApiModelProperty(value = "數(shù)量")
private Integer amount;
}
對(duì)應(yīng)的 Swagger 如下:
圖片
由于類型定義為 Integer, 所以當(dāng)輸入非法值(ProductTypes 定義之外的值)時(shí),系統(tǒng)仍舊能接受并執(zhí)行后續(xù)流程,這就是最核心的問(wèn)題所在,如下圖所示:
圖片
==商品類型(ProductType)在系統(tǒng)中是一個(gè)字典,有自己的固定取值范圍==,定義為 Integer 將放大可接受的值,一旦值在 ProductType 之外便會(huì)發(fā)生系統(tǒng)異常。
2. 解決方案
針對(duì)這個(gè)案例,小艾可以基于 ProductTypes 中定義的常量對(duì)所有入?yún)⑦M(jìn)行校驗(yàn),并在接入文檔中進(jìn)行強(qiáng)調(diào)。但,隨著系統(tǒng)的發(fā)展肯定會(huì)加入更多的流程,在新流程中產(chǎn)生遺漏就又會(huì)出現(xiàn)同樣的問(wèn)題,那終極解決方案是什么?
將 ProductType 可接受的取值范圍與類型的取值范圍保存一致?。?!
圖片
這正是枚舉重要的應(yīng)用場(chǎng)景。
【原則】規(guī)范、流程 在沒(méi)有檢測(cè)機(jī)制相輔助時(shí)都不可靠。如有可能,請(qǐng)使用編譯器進(jìn)行強(qiáng)制約束?。?!
2.1. 枚舉基礎(chǔ)知識(shí)
關(guān)鍵詞 enum 可以將一組具名值的有限集合創(chuàng)建成一種新的類型,而這些具名的值可以作為常規(guī)程序組件使用。
枚舉最常見(jiàn)的用途便是==替換常量定義==,為其增添類型約束,完成編譯時(shí)類型驗(yàn)證。
2.1.1 枚舉定義
枚舉的定義與類和常量定義非常類似。使用 enum 關(guān)鍵字替換 class 關(guān)鍵字,然后在 enum 中定義“常量”即可。
對(duì)于 ProductType 枚舉方案如下:
// 定義
public enum ProductType {
CLAZZ, BOOK;
}
public class OrderItem{
private ProductType productType;
}
getProductType 和 setProductType 所需類型為 ProductType,不在是比較寬泛的 Integer。在使用的時(shí)候可以通過(guò) ProductType.XXX 的方式獲取對(duì)應(yīng)的枚舉值,這樣對(duì)類型有了更強(qiáng)的限制。
2.1.2. 枚舉的單例性
枚舉值具有單例性,及枚舉中的每個(gè)值都是一個(gè)單例對(duì)象,可以直接使用 == 進(jìn)行等值判斷。
枚舉是定義單例對(duì)象最簡(jiǎn)單的方法。
2.1.3. name 和 ordrial
對(duì)于簡(jiǎn)單的枚舉,存在兩個(gè)維度,一個(gè)是name,即為定義的名稱;一個(gè)是ordinal,即為定義的順序。
圖片
簡(jiǎn)單測(cè)試如下:
@Test
public void nameTest(){
for (ProductType productType : ProductType.values()){
// 枚舉的name維度
String name = productType.name();
System.out.println("ProductType:" + name);
// 通過(guò)name獲取定義的枚舉
ProductType productType1 = ProductType.valueOf(name);
System.out.println(productType == productType1);
}
}
輸出結(jié)果為:
ProductType:CLAZZ
true
ProductType:BOOK
true
ordrial測(cè)試如下:
@Test
public void ordinalTest(){
for (ProductType productType : ProductType.values()){
// 枚舉的ordinal維度
int ordinal = productType.ordinal();
System.out.println("ProductType:" + ordinal);
// 通過(guò)ordinal獲取定義的枚舉
ProductType productType1 = ProductType.values()[ordinal];
System.out.println(productType == productType1);
}
}
輸出結(jié)果如下:
ProductType:0
true
ProductType:1
true
從輸出上可以清晰的看出:
- name 是我們?cè)诿杜e中定義變量的名稱
- ordrial 是我們?cè)诿杜e中定義變量的順序
2.1.4. 枚舉的本質(zhì)
enum可以理解為編譯器的語(yǔ)法糖,在創(chuàng)建 enum 時(shí),編譯器會(huì)為你生成一個(gè)相關(guān)的類,這個(gè)類繼承自 java.lang.Enum。
先看下Enum提供了什么:
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;
}
// 枚舉構(gòu)造函數(shù)
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
/**
* 重寫toString方法, 返回枚舉定義名稱
*/
public String toString() {
return name;
}
// 重寫equals,由于枚舉對(duì)象為單例,所以直接使用==進(jìn)行比較
public final boolean equals(Object other) {
return this==other;
}
// 重寫hashCode
public final int hashCode() {
return super.hashCode();
}
/**
* 枚舉為單例對(duì)象,不允許clone
*/
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* 重寫compareTo方法,同種類型按照定義順序進(jìn)行比較
*/
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;
}
/**
* 靜態(tài)方法,根據(jù)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() { }
/**
* 枚舉為單例對(duì)象,禁用反序列化
*/
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 中我們可以得到:
- Enum 中對(duì) name 和 ordrial(final)的屬性進(jìn)行定義,并提供構(gòu)造函數(shù)進(jìn)行初始化
- 重寫了equals、hashCode、toString方法,其中toString方法默認(rèn)返回 name
- 實(shí)現(xiàn)了Comparable 接口,重寫 compareTo,使用枚舉定義順序進(jìn)行比較
- 實(shí)現(xiàn)了Serializable 接口,并重寫禁用了clone、readObject 等方法,以保障枚舉的單例性
- 提供 valueOf 方法使用反射機(jī)制,通過(guò)name獲取枚舉值
到此已經(jīng)解釋了枚舉類的大多數(shù)問(wèn)題,ProductType.values(), ProductType.CLAZZ, ProductType.BOOK,又是從怎么來(lái)的呢?這些是編譯器為其添加的。
@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);
}
}
我們分別對(duì) ProductType 中的屬性和方法進(jìn)行打印,結(jié)果如下:
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)
從輸出,我們可知編譯器為我們添加了以下幾個(gè)特性:
- 針對(duì)每一個(gè)定義的枚舉值,添加一個(gè)同名的 public static final 的屬性
- 添加一個(gè)private static final <pre>不能識(shí)別此Latex公式: VALUES 屬性記錄枚舉中所有的值信息
- 添加一個(gè)靜態(tài)的 values 方法,返回枚舉中所有的值信息(</pre>VALUES)
- 添加一個(gè)靜態(tài)的 valueOf 方法,用于通過(guò) name 獲取枚舉值(調(diào)用 Enum 中的 valueOf 方法)
2.2. 修復(fù)方案
了解枚舉的基礎(chǔ)知識(shí)后,落地方案也就變的非常簡(jiǎn)單,只需:
- 構(gòu)建一個(gè)枚舉類 ProductType,將所有支持的類型添加到枚舉中;
- 將原來(lái) OrderItem 中的 productType 從原來(lái)的 Integer 替換為 ProductType;
具體代碼如下:
// 將產(chǎn)品類型定義為 枚舉
public enum ProductType {
CLAZZ, BOOK; // 定義系統(tǒng)所支持的類型
}
// 領(lǐng)域?qū)ο笾兄苯邮褂?ProductType 枚舉
public class OrderItem{
// 將原來(lái)的 Integer 替換為 ProductType
private ProductType productType;
}
// 創(chuàng)建單個(gè)訂單的請(qǐng)求對(duì)象
@Data
@ApiModel(description = "創(chuàng)建單個(gè)訂單")
class CreateOrderRequest {
@ApiModelProperty(value = "產(chǎn)品類型")
private ProductType productType;
@ApiModelProperty(value = "產(chǎn)品id")
private Integer productId;
@ApiModelProperty(value = "數(shù)量")
private Integer amount;
}
新的 Swagger 如下:
圖片
可見(jiàn),ProductType 被定義為枚舉類型,并直接給出了全部備選項(xiàng)。
3. 更多應(yīng)用場(chǎng)景
枚舉的核心是==具有固定值的集合==,非常適用于各種類型(Type)、狀態(tài)(Status) 這些場(chǎng)景,所以在系統(tǒng)中看到 Type、Status、State 等關(guān)鍵字時(shí),需要慎重考慮是否可以使用枚舉。
但,枚舉作為一種特殊的類,也為很多場(chǎng)景提供了更優(yōu)雅的解決方案。
3.1. Switch
在Java 1.5之前,只有一些簡(jiǎn)單類型(int,short,char,byte)可以用于 switch 的 case 語(yǔ)句,我們習(xí)慣采用 ‘常量+case’ 的方式增加代碼的可讀性,但是丟失了類型系統(tǒng)的校驗(yàn)。由于枚舉的 ordinal 特性的存在,可以將其用于case語(yǔ)句。
public class FruitConstant {
public static final int APPLE = 1;
public static final int BANANA = 2;
public static final int PEAR = 3;
}
// 沒(méi)有類型保障
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中單例的編寫主要有餓漢式、懶漢式、靜態(tài)內(nèi)部類等幾種方式(雙重鎖判斷存在缺陷),但還有一種簡(jiǎn)單的方式是基于枚舉的單例。
public interface Converter<S, T> {
T convert(S source);
}
// 每一個(gè)枚舉值都是一個(gè)單例對(duì)象
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. 狀態(tài)機(jī)
狀態(tài)機(jī)是解決業(yè)務(wù)流程中的一種有效手段,而枚舉的單例性,為構(gòu)建狀態(tài)機(jī)提供了便利。
以下是一個(gè)訂單的狀態(tài)扭轉(zhuǎn)流程,所涉及的狀態(tài)包括 Created、Canceled、Confirmed、Overtime、Paied;所涉及的動(dòng)作包括cancel、confirm、timeout、pay。
graph TB
None{開(kāi)始}--> |create|Created
Created-->|confirm|Confirmed
Created-->|cancel|Canceld
Confirmed-->|cancel|Canceld
Confirmed-->|timeout|Overtime
Confirmed-->|pay| Paied
// 狀態(tài)操作接口,管理所有支持的動(dòng)作
public interface IOrderState {
void cancel(OrderStateContext context);
void confirm(OrderStateContext context);
void timeout(OrderStateContext context);
void pay(OrderStateContext context);
}
// 狀態(tài)機(jī)上下文
public interface OrderStateContext {
void setStats(OrderState state);
}
// 訂單實(shí)際實(shí)現(xiàn)
public class Order{
private OrderState state;
private void setStats(OrderState state) {
this.state = state;
}
// 將請(qǐng)求轉(zhuǎn)發(fā)給狀態(tài)機(jī)
public void cancel() {
this.state.cancel(new StateContext());
}
// 將請(qǐng)求轉(zhuǎn)發(fā)給狀態(tài)機(jī)
public void confirm() {
this.state.confirm(new StateContext());
}
// 將請(qǐng)求轉(zhuǎn)發(fā)給狀態(tài)機(jī)
public void timeout() {
this.state.timeout(new StateContext());
}
// 將請(qǐng)求轉(zhuǎn)發(fā)給狀態(tài)機(jī)
public void pay() {
this.state.pay(new StateContext());
}
// 內(nèi)部類,實(shí)現(xiàn)OrderStateContext,回寫Order的狀態(tài)
class StateContext implements OrderStateContext{
@Override
public void setStats(OrderState state) {
Order.this.setStats(state);
}
}
}
// 基于枚舉的狀態(tài)機(jī)實(shí)現(xiàn)
public enum OrderState implements IOrderState{
CREATED{
// 允許進(jìn)行cancel操作,并把狀態(tài)設(shè)置為CANCELD
@Override
public void cancel(OrderStateContext context){
context.setStats(CANCELD);
}
// 允許進(jìn)行confirm操作,并把狀態(tài)設(shè)置為CONFIRMED
@Override
public void confirm(OrderStateContext context) {
context.setStats(CONFIRMED);
}
},
CONFIRMED{
// 允許進(jìn)行cancel操作,并把狀態(tài)設(shè)置為CANCELD
@Override
public void cancel(OrderStateContext context) {
context.setStats(CANCELD);
}
// 允許進(jìn)行timeout操作,并把狀態(tài)設(shè)置為OVERTIME
@Override
public void timeout(OrderStateContext context) {
context.setStats(OVERTIME);
}
// 允許進(jìn)行pay操作,并把狀態(tài)設(shè)置為PAIED
@Override
public void pay(OrderStateContext context) {
context.setStats(PAIED);
}
},
// 最終狀態(tài),不允許任何操作
CANCELD{
},
// 最終狀態(tài),不允許任何操作
OVERTIME{
},
// 最終狀態(tài),不允許任何操作
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. 責(zé)任鏈
在責(zé)任鏈模式中,程序可以使用多種方式來(lái)處理一個(gè)問(wèn)題,然后把他們鏈接起來(lái),當(dāng)一個(gè)請(qǐng)求進(jìn)來(lái)后,他會(huì)遍歷整個(gè)鏈,找到能夠處理該請(qǐng)求的處理器并對(duì)請(qǐng)求進(jìn)行處理。
枚舉可以實(shí)現(xiàn)某個(gè)接口,加上其天生的單例特性,可以成為組織責(zé)任鏈處理器的一種方式。
// 消息類型
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);
// 如果消息體是接受類型,調(diào)用doHandle進(jìn)行業(yè)務(wù)處理
@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. 分發(fā)器
分發(fā)器根據(jù)輸入的數(shù)據(jù),找到對(duì)應(yīng)的處理器,并將請(qǐng)求轉(zhuǎn)發(fā)給處理器進(jìn)行處理。 由于 EnumMap 其出色的性能,特別適合根據(jù)特定類型作為分發(fā)策略的場(chǎng)景。
// 消息體
@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,
// 進(jìn)入房間
ENTER_ROOM,
// 退出房間
EXIT_ROOM,
// 登出
LOGOUT;
}
// 消息處理器
public interface MessageHandler {
void handle(Message message);
}
// 基于EnumMap的消息分發(fā)器
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. 示例&源碼
倉(cāng)庫(kù)地址:https://gitee.com/litao851025/learnFromBug/
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/limit