小艾剛剛和大飛哥炒了一架,心情非常低落。整個事情是這樣,小艾前段時間剛剛接手訂單系統,今天收到一大波線上 NPE (Null Pointer Exception)報警,經排查發現訂單表的商品類型(ProductType)出現一組非法值,在展示訂單時由于系統無法識別這些非法值導致空指針異常。小艾通過排查,發現訂單來自于市場團隊,于是找到團隊負責人大飛哥,并把現狀和排查結果進行同步。經過大飛哥的排查,確實是在前端的各種跳轉過程中導致 商品類型參數 被覆蓋,立即安排緊急上線進行修復。整個事情處理速度快也沒造成太大損失,但在事故復盤過程中出現了偏差:
兩人各持己見爭論不休,你認為責任在誰呢?
在訂單系統中,商品類型定義為 Integer 類型,使用靜態常量來表示系統所支持的具體值,核心代碼如下:
// 領域對象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 如下:
圖片
由于類型定義為 Integer, 所以當輸入非法值(ProductTypes 定義之外的值)時,系統仍舊能接受并執行后續流程,這就是最核心的問題所在,如下圖所示:
圖片
==商品類型(ProductType)在系統中是一個字典,有自己的固定取值范圍==,定義為 Integer 將放大可接受的值,一旦值在 ProductType 之外便會發生系統異常。
針對這個案例,小艾可以基于 ProductTypes 中定義的常量對所有入參進行校驗,并在接入文檔中進行強調。但,隨著系統的發展肯定會加入更多的流程,在新流程中產生遺漏就又會出現同樣的問題,那終極解決方案是什么?
將 ProductType 可接受的取值范圍與類型的取值范圍保存一致!!!
圖片
這正是枚舉重要的應用場景。
【原則】規范、流程 在沒有檢測機制相輔助時都不可靠。如有可能,請使用編譯器進行強制約束!!!
關鍵詞 enum 可以將一組具名值的有限集合創建成一種新的類型,而這些具名的值可以作為常規程序組件使用。
枚舉最常見的用途便是==替換常量定義==,為其增添類型約束,完成編譯時類型驗證。
枚舉的定義與類和常量定義非常類似。使用 enum 關鍵字替換 class 關鍵字,然后在 enum 中定義“常量”即可。
對于 ProductType 枚舉方案如下:
// 定義public enum ProductType { CLAZZ, BOOK;}public class OrderItem{ private ProductType productType;}
getProductType 和 setProductType 所需類型為 ProductType,不在是比較寬泛的 Integer。在使用的時候可以通過 ProductType.XXX 的方式獲取對應的枚舉值,這樣對類型有了更強的限制。
枚舉值具有單例性,及枚舉中的每個值都是一個單例對象,可以直接使用 == 進行等值判斷。
枚舉是定義單例對象最簡單的方法。
對于簡單的枚舉,存在兩個維度,一個是name,即為定義的名稱;一個是ordinal,即為定義的順序。
圖片
簡單測試如下:
@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); }}
輸出結果為:
ProductType:CLAZZtrueProductType:BOOKtrue
ordrial測試如下:
@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); }}
輸出結果如下:
ProductType:0trueProductType:1true
從輸出上可以清晰的看出:
enum可以理解為編譯器的語法糖,在創建 enum 時,編譯器會為你生成一個相關的類,這個類繼承自 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; } // 枚舉構造函數 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 中我們可以得到:
到此已經解釋了枚舉類的大多數問題,ProductType.values(), ProductType.CLAZZ, ProductType.BOOK,又是從怎么來的呢?這些是編譯器為其添加的。
@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 中的屬性和方法進行打印,結果如下:
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)
從輸出,我們可知編譯器為我們添加了以下幾個特性:
了解枚舉的基礎知識后,落地方案也就變的非常簡單,只需:
具體代碼如下:
// 將產品類型定義為 枚舉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 被定義為枚舉類型,并直接給出了全部備選項。
枚舉的核心是==具有固定值的集合==,非常適用于各種類型(Type)、狀態(Status) 這些場景,所以在系統中看到 Type、Status、State 等關鍵字時,需要慎重考慮是否可以使用枚舉。
但,枚舉作為一種特殊的類,也為很多場景提供了更優雅的解決方案。
在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 "未知";}
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)); }}
狀態機是解決業務流程中的一種有效手段,而枚舉的單例性,為構建狀態機提供了便利。
以下是一個訂單的狀態扭轉流程,所涉及的狀態包括 Created、Canceled、Confirmed、Overtime、Paied;所涉及的動作包括cancel、confirm、timeout、pay。
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(); }}
在責任鏈模式中,程序可以使用多種方式來處理一個問題,然后把他們鏈接起來,當一個請求進來后,他會遍歷整個鏈,找到能夠處理該請求的處理器并對請求進行處理。
枚舉可以實現某個接口,加上其天生的單例特性,可以成為組織責任鏈處理器的一種方式。
// 消息類型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; }}
分發器根據輸入的數據,找到對應的處理器,并將請求轉發給處理器進行處理。 由于 EnumMap 其出色的性能,特別適合根據特定類型作為分發策略的場景。
// 消息體@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); } }}
倉庫地址:https://gitee.com/litao851025/learnFromBug/
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/limit
本文鏈接:http://www.www897cc.com/showinfo-26-41674-0.html【故障現場】控制好取值范圍,甭給別人犯錯的機會
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 昇騰 AI 開發者創享日?廣州站成功舉辦 四大儀式激發人工智能產業創新活力
下一篇: 聊聊分布式數據庫TDSQL的技術架構