GitHub 18k Star 的Java工程師成神之路,不來了解一下嗎!java
GitHub 18k Star 的Java工程師成神之路,真的不來了解一下嗎!git
GitHub 18k Star 的Java工程師成神之路,真的真的不來了解一下嗎!程序員
最近,咱們的線上環境出現了一個問題,線上代碼在執行過程當中拋出了一個IllegalArgumentException,分析堆棧後,發現最根本的的異常是如下內容:github
java.lang.IllegalArgumentException: No enum constant com.a.b.f.m.a.c.AType.P_M
大概就是以上的內容,看起來仍是很簡單的,提示的錯誤信息就是在AType這個枚舉類中沒有找到P_M這個枚舉項。框架
因而通過排查,咱們發現,在線上開始有這個異常以前,該應用依賴的一個下游系統有發佈,而發佈過程當中是一個API包發生了變化,主要變化內容是在一個RPC接口的Response返回值類中的一個枚舉參數AType中增長了P_M這個枚舉項。code
可是下游系統發佈時,並未通知到咱們負責的這個系統進行升級,因此就報錯了。對象
咱們來分析下爲何會發生這樣的狀況。blog
首先,下游系統A提供了一個二方庫的某一個接口的返回值中有一個參數類型是枚舉類型。接口
一方庫指的是本項目中的依賴 二方庫指的是公司內部其餘項目提供的依賴 三方庫指的是其餘組織、公司等來自第三方的依賴開發
public interface AFacadeService { public AResponse doSth(ARequest aRequest); } public Class AResponse{ private Boolean success; private AType aType; } public enum AType{ P_T, A_B }
而後B系統依賴了這個二方庫,而且會經過RPC遠程調用的方式調用AFacadeService的doSth方法。
public class BService { @Autowired AFacadeService aFacadeService; public void doSth(){ ARequest aRequest = new ARequest(); AResponse aResponse = aFacadeService.doSth(aRequest); AType aType = aResponse.getAType(); } }
這時候,若是A和B系統依賴的都是同一個二方庫的話,二者使用到的枚舉AType會是同一個類,裏面的枚舉項也都是一致的,這種狀況不會有什麼問題。
可是,若是有一天,這個二方庫作了升級,在AType這個枚舉類中增長了一個新的枚舉項P_M,這時候只有系統A作了升級,可是系統B並無作升級。
那麼A系統依賴的的AType就是這樣的:
public enum AType{ P_T, A_B, P_M }
而B系統依賴的AType則是這樣的:
public enum AType{ P_T, A_B }
這種狀況下,在B系統經過RPC調用A系統的時候,若是A系統返回的AResponse中的aType的類型位新增的P_M時候,B系統就會沒法解析。通常在這種時候,RPC框架就會發生反序列化異常。致使程序被中斷。
這個問題的現象咱們分析清楚了,那麼再來看下原理是怎樣的,爲何出現這樣的異常呢。
其實這個原理也不難,這類RPC框架大多數會採用JSON的格式進行數據傳輸,也就是客戶端會將返回值序列化成JSON字符串,而服務端會再將JSON字符串反序列化成一個Java對象。
而JSON在反序列化的過程當中,對於一個枚舉類型,會嘗試調用對應的枚舉類的valueOf方法來獲取到對應的枚舉。
而咱們查看枚舉類的valueOf方法的實現時,就能夠發現,若是從枚舉類中找不到對應的枚舉項的時候,就會拋出IllegalArgumentException:
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); }
關於這個問題,其實在《阿里巴巴Java開發手冊》中也有相似的約定:

這裏面規定"對於二方庫的參數可使用枚舉,可是返回值不容許使用枚舉"。這背後的思考就是本文上面提到的內容。
爲何參數中能夠有枚舉?
不知道你們有沒有想過這個問題,其實這個就和二方庫的職責有點關係了。
通常狀況下,A系統想要提供一個遠程接口給別人調用的時候,就會定義一個二方庫,告訴其調用方如何構造參數,調用哪一個接口。
而這個二方庫的調用方會根據其中定義的內容來進行調用。而參數的構造過程是由B系統完成的,若是B系統使用到的是一箇舊的二方庫,使用到的枚舉天然是已有的一些,新增的就不會被用到,因此這樣也不會出現問題。
好比前面的例子,B系統在調用A系統的時候,構造參數的時候使用到AType的時候就只有P_T和A_B兩個選項,雖然A系統已經支持P_M了,可是B系統並無使用到。
若是B系統想要使用P_M,那麼就須要對該二方庫進行升級。
可是,返回值就不同了,返回值並不受客戶端控制,服務端返回什麼內容是根據他本身依賴的二方庫決定的。
可是,其實相比較於手冊中的規定,我更加傾向於,在RPC的接口中入參和出參都不要使用枚舉。
通常,咱們要使用枚舉都是有幾個考慮:
一、枚舉嚴格控制下游系統的傳入內容,避免非法字符。
二、方便下游系統知道均可以傳哪些值,不容易出錯。
不能否認,使用枚舉確實有一些好處,可是我不建議使用主要有如下緣由:
一、若是二方庫升級,而且刪除了一個枚舉中的部分枚舉項,那麼入參中使用枚舉也會出現問題,調用方將沒法識別該枚舉項。
二、有的時候,上下游系統有多個,如C系統經過B系統間接調用A系統,A系統的參數是由C系統傳過來的,B系統只是作了一個參數的轉換與組裝。這種狀況下,一旦A系統的二方庫升級,那麼B和C都要同時升級,任何一個不升級都將沒法兼容。
我其實建議你們在接口中使用字符串代替枚舉,相比較於枚舉這種強類型,字符串算是一種弱類型。
若是使用字符串代替RPC接口中的枚舉,那麼就能夠避免上面咱們提到的兩個問題,上游系統只須要傳遞字符串就好了,而具體的值的合法性,只須要在A系統內本身進行校驗就能夠了。
爲了方便調用者使用,可使用javadoc的@see註解代表這個字符串字段的取值從那個枚舉中獲取。
public Class AResponse{ private Boolean success; /** * @see AType */ private String aType; }
對於像阿里這種比較龐大的互聯網公司,隨便提供出去的一個接口,可能有上百個調用方,而接口升級也是常態,咱們根本作不到每次二方庫升級以後要求全部調用者跟着一塊兒升級,這是徹底不現實的,而且對於有些調用者來講,他用不到新特性,徹底不必作升級。
還有一種看起來比較特殊,可是實際上比較常見的狀況,就是有的時候一個接口的聲明在A包中,而一些枚舉常量定義在B包中,比較常見的就是阿里的交易相關的信息,訂單分不少層次,每次引入一個包的同時都須要引入幾十個包。
對於調用者來講,我確定是不但願個人系統引入太多的依賴的,一方面依賴多了會致使應用的編譯過程很慢,而且很容易出現依賴衝突問題。
因此,在調用下游接口的時候,若是參數中字段的類型是枚舉的話,那我沒辦法,必須得依賴他的二方庫。可是若是不是枚舉,只是一個字符串,那我就能夠選擇不依賴。
因此,咱們在定義接口的時候,會盡可能避免使用枚舉這種強類型。規範中規定在返回值中不容許使用,而我本身要求更高,就是即便在接口的入參中我也不多使用。
最後,我只是不建議在對外提供的接口的出入參中使用枚舉,並非說完全不要用枚舉,我以前不少文章也提到過,枚舉有不少好處,我在代碼中也常用。因此,切不可因噎廢食。
固然,文中的觀點僅表明我我的,具體是是否是適用其餘人,其餘場景或者其餘公司的實踐,須要讀者們自行分辨下,建議你們在使用的時候能夠多思考一下。
關於做者:Hollis,一個對Coding有着獨特追求的人,阿里巴巴技術專家,《程序員的三門課》聯合做者,《Java工程師成神之路》系列文章做者。
若是您有任何意見、建議,或者想與做者交流,均可以關注公衆號【Hollis】,直接後臺給我留言。
本文由博客一文多發平臺 OpenWrite 發佈!