求你了仇哆,不要再在對(duì)外接口中使用枚舉類型了沦辙!

image.png

最近,我們的線上環(huán)境出現(xiàn)了一個(gè)問題讹剔,線上代碼在執(zhí)行過程中拋出了一個(gè)IllegalArgumentException油讯,分析堆棧后,發(fā)現(xiàn)最根本的的異常是以下內(nèi)容:

java.lang.IllegalArgumentException: 
No enum constant com.a.b.f.m.a.c.AType.P_M

大概就是以上的內(nèi)容延欠,看起來還是很簡(jiǎn)單的陌兑,提示的錯(cuò)誤信息就是在AType這個(gè)枚舉類中沒有找到P_M這個(gè)枚舉項(xiàng)。

于是經(jīng)過排查由捎,我們發(fā)現(xiàn)兔综,在線上開始有這個(gè)異常之前,該應(yīng)用依賴的一個(gè)下游系統(tǒng)有發(fā)布狞玛,而發(fā)布過程中是一個(gè)API包發(fā)生了變化软驰,主要變化內(nèi)容是在一個(gè)RPC接口的Response返回值類中的一個(gè)枚舉參數(shù)AType中增加了P_M這個(gè)枚舉項(xiàng)。

但是下游系統(tǒng)發(fā)布時(shí)为居,并未通知到我們負(fù)責(zé)的這個(gè)系統(tǒng)進(jìn)行升級(jí)碌宴,所以就報(bào)錯(cuò)了。

我們來分析下為什么會(huì)發(fā)生這樣的情況蒙畴。

問題重現(xiàn)

首先贰镣,下游系統(tǒng)A提供了一個(gè)二方庫的某一個(gè)接口的返回值中有一個(gè)參數(shù)類型是枚舉類型呜象。

一方庫指的是本項(xiàng)目中的依賴 二方庫指的是公司內(nèi)部其他項(xiàng)目提供的依賴 三方庫指的是其他組織、公司等來自第三方的依賴

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系統(tǒng)依賴了這個(gè)二方庫碑隆,并且會(huì)通過RPC遠(yuǎn)程調(diào)用的方式調(diào)用AFacadeService的doSth方法恭陡。

public class BService {

    @Autowired
    AFacadeService aFacadeService;

    public void doSth(){
        ARequest aRequest = new ARequest();

        AResponse aResponse = aFacadeService.doSth(aRequest);

        AType aType = aResponse.getAType();
    }
}

這時(shí)候,如果A和B系統(tǒng)依賴的都是同一個(gè)二方庫的話上煤,兩者使用到的枚舉AType會(huì)是同一個(gè)類休玩,里面的枚舉項(xiàng)也都是一致的,這種情況不會(huì)有什么問題劫狠。

但是拴疤,如果有一天,這個(gè)二方庫做了升級(jí)独泞,在AType這個(gè)枚舉類中增加了一個(gè)新的枚舉項(xiàng)P_M呐矾,這時(shí)候只有系統(tǒng)A做了升級(jí),但是系統(tǒng)B并沒有做升級(jí)懦砂。

那么A系統(tǒng)依賴的的AType就是這樣的:

public enum AType{

    P_T,

    A_B,

    P_M
}

而B系統(tǒng)依賴的AType則是這樣的:

public enum AType{

    P_T,

    A_B
}

這種情況下蜒犯,在B系統(tǒng)通過RPC調(diào)用A系統(tǒng)的時(shí)候,如果A系統(tǒng)返回的AResponse中的aType的類型位新增的P_M時(shí)候荞膘,B系統(tǒng)就會(huì)無法解析罚随。一般在這種時(shí)候,RPC框架就會(huì)發(fā)生反序列化異常羽资。導(dǎo)致程序被中斷淘菩。

原理分析

這個(gè)問題的現(xiàn)象我們分析清楚了,那么再來看下原理是怎樣的削罩,為什么出現(xiàn)這樣的異常呢瞄勾。

其實(shí)這個(gè)原理也不難费奸,這類RPC框架大多數(shù)會(huì)采用JSON的格式進(jìn)行數(shù)據(jù)傳輸弥激,也就是客戶端會(huì)將返回值序列化成JSON字符串,而服務(wù)端會(huì)再將JSON字符串反序列化成一個(gè)Java對(duì)象愿阐。

而JSON在反序列化的過程中微服,對(duì)于一個(gè)枚舉類型,會(huì)嘗試調(diào)用對(duì)應(yīng)的枚舉類的valueOf方法來獲取到對(duì)應(yīng)的枚舉缨历。

而我們查看枚舉類的valueOf方法的實(shí)現(xiàn)時(shí)以蕴,就可以發(fā)現(xiàn),如果從枚舉類中找不到對(duì)應(yīng)的枚舉項(xiàng)的時(shí)候辛孵,就會(huì)拋出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);
}

擴(kuò)展思考

為什么參數(shù)中可以有枚舉丛肮?

不知道大家有沒有想過這個(gè)問題,其實(shí)這個(gè)就和二方庫的職責(zé)有點(diǎn)關(guān)系了魄缚。

一般情況下宝与,A系統(tǒng)想要提供一個(gè)遠(yuǎn)程接口給別人調(diào)用的時(shí)候焚廊,就會(huì)定義一個(gè)二方庫,告訴其調(diào)用方如何構(gòu)造參數(shù)习劫,調(diào)用哪個(gè)接口咆瘟。

而這個(gè)二方庫的調(diào)用方會(huì)根據(jù)其中定義的內(nèi)容來進(jìn)行調(diào)用。而參數(shù)的構(gòu)造過程是由B系統(tǒng)完成的诽里,如果B系統(tǒng)使用到的是一個(gè)舊的二方庫袒餐,使用到的枚舉自然是已有的一些,新增的就不會(huì)被用到谤狡,所以這樣也不會(huì)出現(xiàn)問題灸眼。

比如前面的例子,B系統(tǒng)在調(diào)用A系統(tǒng)的時(shí)候墓懂,構(gòu)造參數(shù)的時(shí)候使用到AType的時(shí)候就只有P_T和A_B兩個(gè)選項(xiàng)幢炸,雖然A系統(tǒng)已經(jīng)支持P_M了,但是B系統(tǒng)并沒有使用到拒贱。

如果B系統(tǒng)想要使用P_M宛徊,那么就需要對(duì)該二方庫進(jìn)行升級(jí)。

但是逻澳,返回值就不一樣了闸天,返回值并不受客戶端控制,服務(wù)端返回什么內(nèi)容是根據(jù)他自己依賴的二方庫決定的斜做。

但是苞氮,其實(shí)相比較于手冊(cè)中的規(guī)定,我更加傾向于瓤逼,在RPC的接口中入?yún)⒑统鰠⒍疾灰褂妹杜e笼吟。

一般,我們要使用枚舉都是有幾個(gè)考慮:

1霸旗、枚舉嚴(yán)格控制下游系統(tǒng)的傳入內(nèi)容贷帮,避免非法字符。
2诱告、方便下游系統(tǒng)知道都可以傳哪些值撵枢,不容易出錯(cuò)。

不可否認(rèn)精居,使用枚舉確實(shí)有一些好處锄禽,但是我不建議使用主要有以下原因:

1、如果二方庫升級(jí)靴姿,并且刪除了一個(gè)枚舉中的部分枚舉項(xiàng)沃但,那么入?yún)⒅惺褂妹杜e也會(huì)出現(xiàn)問題,調(diào)用方將無法識(shí)別該枚舉項(xiàng)佛吓。
2宵晚、有的時(shí)候恨旱,上下游系統(tǒng)有多個(gè),如C系統(tǒng)通過B系統(tǒng)間接調(diào)用A系統(tǒng)坝疼,A系統(tǒng)的參數(shù)是由C系統(tǒng)傳過來的搜贤,B系統(tǒng)只是做了一個(gè)參數(shù)的轉(zhuǎn)換與組裝。這種情況下钝凶,一旦A系統(tǒng)的二方庫升級(jí)仪芒,那么B和C都要同時(shí)升級(jí),任何一個(gè)不升級(jí)都將無法兼容耕陷。

我其實(shí)建議大家在接口中使用字符串代替枚舉掂名,相比較于枚舉這種強(qiáng)類型,字符串算是一種弱類型哟沫。

如果使用字符串代替RPC接口中的枚舉饺蔑,那么就可以避免上面我們提到的兩個(gè)問題,上游系統(tǒng)只需要傳遞字符串就行了嗜诀,而具體的值的合法性猾警,只需要在A系統(tǒng)內(nèi)自己進(jìn)行校驗(yàn)就可以了。

為了方便調(diào)用者使用隆敢,可以使用javadoc的@see注解表明這個(gè)字符串字段的取值從那個(gè)枚舉中獲取发皿。

public Class AResponse{

    private Boolean success;

    /**
    *  @see AType 
    */
    private String aType;
}

對(duì)于像阿里這種比較龐大的互聯(lián)網(wǎng)公司,隨便提供出去的一個(gè)接口拂蝎,可能有上百個(gè)調(diào)用方穴墅,而接口升級(jí)也是常態(tài),我們根本做不到每次二方庫升級(jí)之后要求所有調(diào)用者跟著一起升級(jí)温自,這是完全不現(xiàn)實(shí)的玄货,并且對(duì)于有些調(diào)用者來說,他用不到新特性悼泌,完全沒必要做升級(jí)松捉。

還有一種看起來比較特殊,但是實(shí)際上比較常見的情況券躁,就是有的時(shí)候一個(gè)接口的聲明在A包中惩坑,而一些枚舉常量定義在B包中掉盅,比較常見的就是阿里的交易相關(guān)的信息也拜,訂單分很多層次,每次引入一個(gè)包的同時(shí)都需要引入幾十個(gè)包趾痘。

對(duì)于調(diào)用者來說慢哈,我肯定是不希望我的系統(tǒng)引入太多的依賴的,一方面依賴多了會(huì)導(dǎo)致應(yīng)用的編譯過程很慢,并且很容易出現(xiàn)依賴沖突問題。

所以牡直,在調(diào)用下游接口的時(shí)候壁涎,如果參數(shù)中字段的類型是枚舉的話歧寺,那我沒辦法拂共,必須得依賴他的二方庫雕欺。但是如果不是枚舉聋丝,只是一個(gè)字符串编振,那我就可以選擇不依賴缀辩。

所以,我們?cè)诙x接口的時(shí)候踪央,會(huì)盡量避免使用枚舉這種強(qiáng)類型臀玄。規(guī)范中規(guī)定在返回值中不允許使用,而我自己要求更高畅蹂,就是即使在接口的入?yún)⒅形乙埠苌偈褂谩?/p>

最后健无,我只是不建議在對(duì)外提供的接口的出入?yún)⒅惺褂妹杜e,并不是說徹底不要用枚舉液斜,我之前很多文章也提到過累贤,枚舉有很多好處,我在代碼中也經(jīng)常使用少漆。所以畦浓,切不可因噎廢食。

當(dāng)然检疫,文中的觀點(diǎn)僅代表我個(gè)人讶请,具體是是不是適用其他人,其他場(chǎng)景或者其他公司的實(shí)踐屎媳,需要讀者們自行分辨下夺溢,建議大家在使用的時(shí)候可以多思考一下。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末烛谊,一起剝皮案震驚了整個(gè)濱河市风响,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丹禀,老刑警劉巖状勤,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異双泪,居然都是意外死亡持搜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門焙矛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來葫盼,“玉大人,你說我怎么就攤上這事村斟∑兜迹” “怎么了抛猫?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長孩灯。 經(jīng)常有香客問我闺金,道長,這世上最難降的妖魔是什么峰档? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任掖看,我火速辦了婚禮,結(jié)果婚禮上面哥,老公的妹妹穿的比我還像新娘哎壳。我一直安慰自己,他們只是感情好尚卫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布归榕。 她就那樣靜靜地躺著,像睡著了一般吱涉。 火紅的嫁衣襯著肌膚如雪刹泄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天怎爵,我揣著相機(jī)與錄音特石,去河邊找鬼。 笑死鳖链,一個(gè)胖子當(dāng)著我的面吹牛姆蘸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芙委,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逞敷,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了灌侣?” 一聲冷哼從身側(cè)響起推捐,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎侧啼,沒想到半個(gè)月后牛柒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡痊乾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年皮壁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片符喝。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闪彼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出协饲,到底是詐尸還是另有隱情畏腕,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布茉稠,位于F島的核電站描馅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏而线。R本人自食惡果不足惜铭污,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望膀篮。 院中可真熱鬧嘹狞,春花似錦、人聲如沸誓竿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筷屡。三九已至涧偷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毙死,已是汗流浹背燎潮。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扼倘,地道東北人确封。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像再菊,于是被迫代替她去往敵國和親隅肥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容