我為什麼反對用異常做流程控制?

曲總 技術瑣話


我為什麼反對用異常做流程控制?


我為什麼反對用異常做流程控制?

“懶”是驅動程序員前進的原動力,亦是原罪。


像SSH/M這種基礎框架的出現,讓不少程序員“癱瘓”成了流水線工人。以前小心翼翼方能寫就的邏輯分支判斷,演變成了直接丟個異常然後坐等AOP攔截處理,此時的攔截器就是個垃圾處理廠。這種似乎失控的編碼方式,讓我想到了邪惡的“GoTo”語法,很多編程語言裡都有它, 但是都不建議你用它。因為邪惡的不是GoTo本身,而是濫用GoTo的我們。


題眼基本表達了我的論點,隨著本文的深入會對該論點做加一個約束條件。現在容我開始論證它~


都說拋異常很重,到底重在哪裡?


不整虛的,我們用測試數據來說話。採用OpenJDK的JMH基準測試框架實現,設計如下6種測試場景:

  1. New一個普通的Exception
  2. New一個普通的不包含堆棧信息的Exception
  3. New一個普通的自定義對象
  4. Throw一個普通的Exception
  5. Throw一個普通的不包含堆棧信息的Exception
  6. 獲取/打印異常的堆棧信息
我為什麼反對用異常做流程控制?

我為什麼反對用異常做流程控制?

我為什麼反對用異常做流程控制?

我為什麼反對用異常做流程控制?

6個場景的benchmark測試報告如上圖。從結果數字可以看出:耗時最短的是創建自定義對象,耗時最長的是獲取異常的堆棧信息。詳細說明幾個要點:


&創建對象:自定義對象 VS 無堆棧異常 VS 普通異常

三者的耗時依次遞增,自定義對象的創建作為基準參照耗時,無堆棧異常創建的耗時是其5倍,普通異常創建的耗時是其250倍。所以異常從出生就死在起跑線。雖然我們的測試耗時是納秒級別,若從系統接口通常的秒為單位,就算30倍也可以忽略不計。但是在這裡已經可以凸顯出異常本身的沉重。


&異常的創建到拋出到捕獲

異常的創建 和 疊加異常的拋出捕獲 前後並沒有特別明顯的性能損耗,拋異常的耗時可以忽略不計。

明確概念1:Java中如果不發生異常,try/catch基本是不會造成任何性能損失的(查看字節碼瞭解

異常表)。而一旦發生異常,除了昂貴的異常填充堆棧成本,也就是確認下try block對應異常表記錄的起止代碼行和異常名稱是否一致。上測試結果也表明確實會有性能波動,但其實很小。

我為什麼反對用異常做流程控制?

明確概念2:對於try block內的代碼,Java會阻止指令重排序一類的內存優化手段。所以即使try的性能損耗很小,但是我們仍舊建議try block的邊界越窄越好。


明確概念3:try block的範圍即使很寬,對於堆棧深度來說並無特別影響。因為棧幀的深度取決於不同方法之間的調用關係和次數。


&異常堆棧的獲取/打印

現實喜歡狠狠的打人臉,原以為測試出真相了,結果數據告訴我們最耗時的操作竟是讀取堆棧操作。

我為什麼反對用異常做流程控制?

Thread::getStackTrace()做個簡單說明。大家可以看一下JDK源碼,在當前線程裡它等同於

(new Exception()).getStackTrace()

實例化一個異常對象已經夠慢了,獲取異常堆棧數據的耗時竟然達到10倍以上。大家想一想不管是自己寫的try/catch代碼塊,還是AOP的攔截器,是不是都會讀取堆棧,然後打印到日誌裡用於排障?


所以異常重不重已經很明確了吧?再貼一遍測試數據感受一下,所有的真相都在此圖了。

我為什麼反對用異常做流程控制?

代碼示例已上傳Github

https://github.com/NicholasQu/snippets


接口設計如何定義異常的邊界?


傳統的接口設計規範說明會包含幾個基本要素:接口名/地址、版本號、請求參數,響應參數。其中應答的響應碼基本都會一一列舉並詳細說明,讓調用方簡單直觀的理解到此接口的服務能力。


當把控制流程的異常嵌入到接口設計裡,隨之問題就來了:

  1. 甚少看到有人能夠在Javadoc裡使用@exception將接口內的異常標註清楚;
  2. 如何權衡選擇正常的應答返回還是拋異常?當接口應答只是true/false的時候,拋異常會是個很匪夷所思的設計;
  3. 當下層方法不斷的拋出各種異常,然後彙總到攔截器裡處理時,或者需要對異常拆開做判斷,再自定義成合理的應答話術;或者將好不容易區分開的不同異常,被整合成了“通用系統異常”無法分辨;這時候的攔截器就是個異常中央處理池,拆就是hardcode,不拆就可能是浪費了之前的異常細顆粒度;
  4. 為了讓代碼不那麼醜陋,自定義的異常通常繼承自RuntimeException。在接口提供方和調用方沒有通過介質(接口設計文檔/對話...)充分溝通清楚的情況下,一個神不知鬼不覺的Runtime異常完全可能造成自身業務邏輯的無法自恰;
  5. 異常具有正常應答無法比擬的分層穿透性,也就是不管間隔多少層,都可以直接穿透到接入層。對於某些異常場景下的代碼實現確實有很好的支撐,反之,成也蕭何敗蕭何,這種強力的穿透能力是否會讓邏輯失控,totally count on you and your team;


綜合上面的這些點,異常作為非常規的設計路數,在沒有足夠的把控能力下,千萬不要無盡蔓延,這種類似“GoTo”式的代碼實現,可能會讓你的系統支離破碎。


我的態度


任何的系統架構設計,都是在不斷的在做天人交戰,利弊權衡。鮮有絕對的對與錯,只有在當前組織環境內相對的合理與不合理。對於異常用作流程控制這件事,我是投反對票。因為即使異常的性能損耗對我們大部分的業務場景可以忽略不計的,但異常在接口中的易被忽視性、不可控的穿透性,就算是高素質的團隊也不一定能完全消除這種風險。既然風險如此大,寧肯讓團隊按部就班老老實實的寫好每一種應答。


承篇頭的論點,重新展開再抽象歸納一下:

任何邏輯判斷的流程控制都不應該用異常來實現,除非那些能明確導致程序中斷/終止的節點。異常務必要明確拋checked還是unchecked,對調用者負責。


分享到:


相關文章: