求你了,不要再在對外介面中使用列舉型別了
最近,我們的線上環境出現了一個問題,線上程式碼在執行過程中丟擲了一個IllegalArgumentException,分析堆疊後,發現最根本的的異常是以下內容:
java.lang.IllegalArgumentException:
Noenumconstantcom.a.b.f.m.a.c.AType.P_M
大概就是以上的內容,看起來還是很簡單的,提示的錯誤資訊就是在AType這個列舉類中沒有找到P_M這個列舉項。
於是經過排查,我們發現,在線上開始有這個異常之前,該應用依賴的一個下游系統有釋出,而釋出過程中是一個API包發生了變化,主要變化內容是在一個RPC介面的Response返回值類中的一個列舉引數AType中增加了P_M這個列舉項。
但是下游系統釋出時,並未通知到我們負責的這個系統進行升級,所以就報錯了。
我們來分析下為什麼會發生這樣的情況。
問題重現
首先,下游系統A提供了一個二方庫的某一個介面的返回值中有一個引數型別是列舉型別。
一方庫指的是本專案中的依賴
二方庫指的是公司內部其他專案提供的依賴
三方庫指的是其他組織、公司等來自第三方的依賴
然後B系統依賴了這個二方庫,並且會通過RPC遠端呼叫的方式呼叫AFacadeService的doSth方法。
這時候,如果A和B系統依賴的都是同一個二方庫的話,兩者使用到的列舉AType會是同一個類,裡面的列舉項也都是一致的,這種情況不會有什麼問題。
但是,如果有一天,這個二方庫做了升級,在AType這個列舉類中增加了一個新的列舉項P_M,這時候只有系統A做了升級,但是系統B並沒有做升級。
那麼A系統依賴的的AType就是這樣的:
而B系統依賴的AType則是這樣的:
這種情況下,在B系統通過RPC呼叫A系統的時候,如果A系統返回的AResponse中的aType的型別為新增的P_M時候,B系統就會無法解析。一般在這種時候,RPC框架就會發生反序列化異常。導致程式被中斷。
原理分析
這個問題的現象我們分析清楚了,那麼再來看下原理是怎樣的,為什麼出現這樣的異常呢。
其實這個原理也不難,這類RPC框架大多數會採用JSON的格式進行資料傳輸,也就是客戶端會將返回值序列化成JSON字串,而服務端會再將JSON字串反序列化成一個Java物件。
而JSON在反序列化的過程中,對於一個列舉型別,會嘗試呼叫對應的列舉類的valueOf方法來獲取到對應的列舉。
而我們檢視列舉類的valueOf方法的實現時,就可以發現,如果從列舉類中找不到對應的列舉項的時候,就會丟擲IllegalArgumentException:
publicstatic<TextendsEnum<T>>TvalueOf(Class<T>enumType, Stringname){
Tresult=enumType.enumConstantDirectory().get(name);
if(result!=null)
returnresult;
if(name==null)
thrownewNullPointerException("Nameisnull");
thrownewIllegalArgumentException(
"Noenumconstant"+enumType.getCanonicalName()+"."+name);
}
關於這個問題,其實在《阿里巴巴Java開發手冊》中也有類似的約定:
這裡面規定"對於二方庫的引數可以使用列舉,但是返回值不允許使用列舉"。這背後的思考就是本文上面提到的內容。
擴充套件思考
為什麼引數中可以有列舉?
不知道大家有沒有想過這個問題,其實這個就和二方庫的職責有點關係了。
一般情況下,A系統想要提供一個遠端介面給別人呼叫的時候,就會定義一個二方庫,告訴其呼叫方如何構造引數,呼叫哪個介面。
而這個二方庫的呼叫方會根據其中定義的內容來進行呼叫。而引數的構造過程是由B系統完成的,如果B系統使用到的是一箇舊的二方庫,使用到的列舉自然是已有的一些,新增的就不會被用到,所以這樣也不會出現問題。
比如前面的例子,B系統在呼叫A系統的時候,構造引數的時候使用到AType的時候就只有P_T和A_B兩個選項,雖然A系統已經支援P_M了,但是B系統並沒有使用到。
如果B系統想要使用P_M,那麼就需要對該二方庫進行升級。
但是,返回值就不一樣了,返回值並不受客戶端控制,服務端返回什麼內容是根據他自己依賴的二方庫決定的。
但是,其實相比較於手冊中的規定,我更加傾向於,在RPC的介面中入參和出參都不要使用列舉。
一般,我們要使用列舉都是有幾個考慮:
- 1、列舉嚴格控制下游系統的傳入內容,避免非法字元。
- 2、方便下游系統知道都可以傳哪些值,不容易出錯。
不可否認,使用列舉確實有一些好處,但是我不建議使用主要有以下原因:
- 1、如果二方庫升級,並且刪除了一個列舉中的部分列舉項,那麼入參中使用列舉也會出現問題,呼叫方將無法識別該列舉項。
- 2、有的時候,上下游系統有多個,如C系統通過B系統間接呼叫A系統,A系統的引數是由C系統傳過來的,B系統只是做了一個引數的轉換與組裝。這種情況下,一旦A系統的二方庫升級,那麼B和C都要同時升級,任何一個不升級都將無法相容。
我其實建議大家在介面中使用字串代替列舉,相比較於列舉這種強型別,字串算是一種弱型別。
如果使用字串代替RPC介面中的列舉,那麼就可以避免上面我們提到的兩個問題,上游系統只需要傳遞字串就行了,而具體的值的合法性,只需要在A系統內自己進行校驗就可以了。
為了方便呼叫者使用,可以使用javadoc的@see註解表明這個字串欄位的取值從那個列舉中獲取。
對於像阿里這種比較龐大的網際網路公司,隨便提供出去的一個介面,可能有上百個呼叫方,而介面升級也是常態,我們根本做不到每次二方庫升級之後要求所有呼叫者跟著一起升級,這是完全不現實的,並且對於有些呼叫者來說,他用不到新特性,完全沒必要做升級。
還有一種看起來比較特殊,但是實際上比較常見的情況,就是有的時候一個介面的宣告在A包中,而一些列舉常量定義在B包中,比較常見的就是阿里的交易相關的資訊,訂單分很多層次,每次引入一個包的同時都需要引入幾十個包。
對於呼叫者來說,我肯定是不希望我的系統引入太多的依賴的,一方面依賴多了會導致應用的編譯過程很慢,並且很容易出現依賴衝突問題。
所以,在呼叫下游介面的時候,如果引數中欄位的型別是列舉的話,那我沒辦法,必須得依賴他的二方庫。但是如果不是列舉,只是一個字串,那我就可以選擇不依賴。
所以,我們在定義介面的時候,會盡量避免使用列舉這種強型別。規範中規定在返回值中不允許使用,而我自己要求更高,就是即使在介面的入參中我也很少使用。
最後,我只是不建議在對外提供的介面的出入參中使用列舉,並不是說徹底不要用列舉,我之前很多文章也提到過,列舉有很多好處,我在程式碼中也經常使用。所以,切不可因噎廢食。
當然,文中的觀點僅代表我個人,具體是是不是適用其他人,其他場景或者其他公司的實踐,需要讀者們自行分辨下,建議大家在使用的時候可以多思考一下。