<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          程序員新人在對(duì)外接口中使用枚舉類型,竟然引發(fā)了生產(chǎn)事故

          共 5103字,需瀏覽 11分鐘

           ·

          2022-07-26 08:22

          二哥的編程星球已經(jīng)有 450 多名 小伙伴加入了,如果你也需要一個(gè)良好的學(xué)習(xí)氛圍,戳鏈接加入我們吧!這是一個(gè) Java 學(xué)習(xí)指南 + 編程實(shí)戰(zhàn)的私密圈子,你可以向二哥提問(wèn)、幫你制定學(xué)習(xí)計(jì)劃、跟著二哥一起做項(xiàng)目、刷力扣,沖沖沖。

          最近,我們的線上環(huán)境出現(xiàn)了一個(gè)問(wèn)題,線上代碼在執(zhí)行過(guò)程中拋出了一個(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)容,看起來(lái)還是很簡(jiǎn)單的,提示的錯(cuò)誤信息就是在 AType 這個(gè)枚舉類中沒(méi)有找到 P_M 這個(gè)枚舉項(xiàng)。

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

          但是,剛?cè)肼氁粋€(gè)月的新同事小二在開發(fā)下游系統(tǒng)發(fā)布時(shí),并未通知到我們負(fù)責(zé)的這個(gè)系統(tǒng)進(jìn)行升級(jí),所以就報(bào)錯(cuò)了。

          對(duì)外接口中使用枚舉類型會(huì)有什么問(wèn)題呢小二很是不解。

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

          問(wèn)題重現(xiàn)

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

          一方庫(kù)指的是本項(xiàng)目中的依賴

          二方庫(kù)指的是公司內(nèi)部其他項(xiàng)目提供的依賴

          三方庫(kù)指的是其他組織、公司等來(lái)自第三方的依賴

          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è)二方庫(kù),并且會(huì)通過(guò) 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è)二方庫(kù)的話,兩者使用到的枚舉 AType 會(huì)是同一個(gè)類,里面的枚舉項(xiàng)也都是一致的,這種情況不會(huì)有什么問(wèn)題。

          但是,如果有一天,這個(gè)二方庫(kù)做了升級(jí),在 AType 這個(gè)枚舉類中增加了一個(gè)新的枚舉項(xiàng) P_M,這時(shí)候只有系統(tǒng) A 做了升級(jí),但是系統(tǒng) B 并沒(méi)有做升級(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)通過(guò) RPC 調(diào)用 A 系統(tǒng)的時(shí)候,如果 A 系統(tǒng)返回的 AResponse 中的 aType 的類型為新增的 P_M 時(shí)候,B 系統(tǒng)就會(huì)無(wú)法解析。一般在這種時(shí)候,RPC 框架就會(huì)發(fā)生反序列化異常。導(dǎo)致程序被中斷。

          原理分析

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

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

          而 JSON 在反序列化的過(guò)程中,對(duì)于一個(gè)枚舉類型,會(huì)嘗試調(diào)用對(duì)應(yīng)的枚舉類的 valueOf 方法來(lái)獲取到對(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);

          }

          關(guān)于這個(gè)問(wèn)題,其實(shí)在《阿里巴巴 Java 開發(fā)手冊(cè)》中也有類似的約定:

          這里面規(guī)定"對(duì)于二方庫(kù)的參數(shù)可以使用枚舉,但是返回值不允許使用枚舉"。這背后的思考就是本文上面提到的內(nèi)容。

          擴(kuò)展思考

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

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

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

          而這個(gè)二方庫(kù)的調(diào)用方會(huì)根據(jù)其中定義的內(nèi)容來(lái)進(jìn)行調(diào)用。而參數(shù)的構(gòu)造過(guò)程是由 B 系統(tǒng)完成的,如果 B 系統(tǒng)使用到的是一個(gè)舊的二方庫(kù),使用到的枚舉自然是已有的一些,新增的就不會(huì)被用到,所以這樣也不會(huì)出現(xiàn)問(wè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)并沒(méi)有使用到。

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

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

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

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

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

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

          • 如果二方庫(kù)升級(jí),并且刪除了一個(gè)枚舉中的部分枚舉項(xiàng),那么入?yún)⒅惺褂妹杜e也會(huì)出現(xiàn)問(wèn)題,調(diào)用方將無(wú)法識(shí)別該枚舉項(xiàng)。
          • 有的時(shí)候,上下游系統(tǒng)有多個(gè),如 C 系統(tǒng)通過(guò) B 系統(tǒng)間接調(diào)用 A 系統(tǒng),A 系統(tǒng)的參數(shù)是由 C 系統(tǒng)傳過(guò)來(lái)的,B 系統(tǒng)只是做了一個(gè)參數(shù)的轉(zhuǎn)換與組裝。這種情況下,一旦 A 系統(tǒng)的二方庫(kù)升級(jí),那么 B 和 C 都要同時(shí)升級(jí),任何一個(gè)不升級(jí)都將無(wú)法兼容。

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

          如果使用字符串代替 RPC 接口中的枚舉,那么就可以避免上面我們提到的兩個(gè)問(wèn)題,上游系統(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),我們根本做不到每次二方庫(kù)升級(jí)之后要求所有調(diào)用者跟著一起升級(jí),這是完全不現(xiàn)實(shí)的,并且對(duì)于有些調(diào)用者來(lái)說(shuō),他用不到新特性,完全沒(méi)必要做升級(jí)。

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

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

          所以,在調(diào)用下游接口的時(shí)候,如果參數(shù)中字段的類型是枚舉的話,那我沒(méi)辦法,必須得依賴他的二方庫(kù)。但是如果不是枚舉,只是一個(gè)字符串,那我就可以選擇不依賴。

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

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

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


          沒(méi)有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧?kù)o的港灣,我是不系之舟

          推薦閱讀

          瀏覽 32
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91麻豆成人精品国产免费网站 | 日韩中文在线视频 | 天天日天天操天天色 | 影音先锋 后妈av | 亚洲色拍视频 |