最近在項目代碼中,遇見異常濫用的情形,分析下會帶來哪些后果。
1. 代碼可讀性變差,業務邏輯難以理解
異常流與業務狀態流混在一起,無法從接口協議層面理解業務代碼,只能深入到方法(Method)內部才能准確理解返回值的行為
可看一下代碼:
public UserProfile findByID(long user_id) { Map<String, Object> cond = new HashMap<String, Object>(); cond.put("id", user_id); UserProfile userInfo = null; try { userInfo = DBUtil.selecta(UserProfile.class, "user_info", cond); } catch (Throwable e) { log.error(e, "UserProfile findByID"); } return userInfo; }
DAO層負責數據庫的基本操作,該方法返回值為查詢結果用戶對象數據。代碼強行抓了所有的異常,並以null返回,后來人無法確認null是代表該用戶不存在還是出現異常。
2. 代碼健壯性變差,異常信息被隨意捕捉,甚至被吃掉
同樣上述代碼,首先抓了Throwable這個所有異常,包括Error(后文會介紹異常體系)。代碼內部隱藏了問題,只是打印了一行日志,並且讓程序可以正常繼續往后走,帶來的不確定性和風險都很大,這也極大的影響代碼的健壯。
3. 破壞架構的分層清晰,職責單一的原則,為系統擴展帶來很大阻礙
隨着系統的發展,往往會沉淀出一些平台系統,比如調用監控。會負責統一采集系統的各類信息,因為這樣的錯誤異常處理,將很難統一分離出異常信息。
那我們在實際編寫代碼的如何正確考慮異常的使用呢?
首先了解下Java異常的設計初衷。
Exceptions are the customary way in Java to indicate to a calling method that an abnormal condition has occurred. 一個很重要的概念——不正常情形。Java異常旨在處理方法調用時不正常情形,但我們該如何理解“不正常情形”?
看下圖,JDK給我們定義了以下異常體系:
根節點是Throwable,代表Java內所有可以Catch的異常都繼承此,向下有兩類,Exception和Error,日常用到較多的都是Exception,Error一般留給JDK內部自己使用,比如內存溢出OutOfMemoryError,這類嚴重的問題,應用進程什么都做不了,只能終止。用戶抓住此類Error,一般無法處理,盡快終止往往是最安全的方式,既然什么都干不了就沒必要抓住了。Exception是應用代碼要重點關心的,其下又分為運行時異常RuntimeException和編譯時異常,各自區分就不在詳述了。
了解異常的結構后,接下來解決兩個重要問題,何時拋異常和拋什么異常,何時抓異常和抓什么異常 何時會有異常拋出,總結起來有以下三個典型的場景:
-
調用方(Client)破壞了協議
說白了就是調用方法時沒有按照約定好的規范來傳參數,典型的比如參數是個非空集合卻傳入了空值。這種破壞協議的還可以細分兩類,一類是調用方從接口形式上不易覺察的規則但需要在出現時給調用方些強提示,帶些信息上去,這時異常就是特別好的方式;另一類是調用方可以明確看到的規則,正常情況會正常處理協議,不會產生破壞,但可能因為bug導致破壞協議。
public void method(String[] args) { int temperature = 0; if (args.length > 0) { try { temperature = Integer.parseInt(args[0]); } catch(NumberFormatException e) { throw new IllegalArgumentException( "Must enter integer as first argument, args[0]="+args[0],e); } } // 其他代碼 }
要求傳入整數,但轉換數字時出錯,此時可拋出特定異常並附上提示信息
-
(Method)知道有問題,但自己處理不了
這里"有問題",可能是調用方破壞了協議,但是單獨提出來是要從被調用方出發考慮,比如Method內部有讀取文件操作,但發現文件並不存在
1 public static void main(String[] args) { 2 if (args.length == 0) { 3 System.out.println("Must give filename as first arg."); 4 return; 5 } 6 FileInputStream in = null; 7 try { 8 in = new FileInputStream(args[0]); 9 } 10 catch (FileNotFoundException e) { 11 System.out.println("Can't find file: " + args[0]); 12 return; 13 } 14 // 其他代碼 15 }
FileInputStream在創建時拋出了FileNotFoundException,顯然出現該問題時FileInputStream是處理不了的。
-
預料不到的情形
空指針異常、數組越界是這種典型的場景,一般是由於有代碼分支被忽略
了解了何時會出現異常,但是需要拋出異常時是選擇編譯時異常還是運行時異常呢? 很多人可能會說,很簡單啊,需要調用方catch的就編譯時,否則運行時。問題來了,什么時候需要調用方catch?
分析編譯時和運行時對代碼編寫的影響,可以總結出來區分時考慮的點有:調用方能否處理、嚴重程度、出現的可能性。
調用方能處理->編譯時
調用方不能處理->運行時
嚴重程度高->運行時
出現可能性低->運行時
本人細化了這個分類的考慮過程如下:
首先從調用方開始考慮,如果是調用方破壞了協議,則拋出運行時異常,這類異常一般出現可能性較低,調用方已知,所以沒必要強制調用方抓此異常。
然后如果問題出現被調用方,無法正常執行完成工作,這時候考慮該問題調用方是否可以處理,如果能處理,比如文件找不到、網絡超時,則拋出編譯時異常,否則比如磁盤滿,拋運行時異常
解決了何時拋異常和拋什么異常,接下來是調用這些有異常的代碼時,何時catch和catch什么異常呢? 攻守不分離... 免不了俗,總結一下幾點供大家探討:
-
不要輕易抓Throwable,圖省事可能會帶來巨大的隱患
1 try { 2 someMethod(); 3 } catch (Throwable e) { 4 log.error("method has failed", e); 5 }
應該盡量只去抓關注的異常,明確catch的都是什么具體的異常
-
自己處理不了,不要抓
比如上文DB可能會有異常,在DAO層是處理不了這種問題的,交由上層處理。抓異常宜晚不宜早,拋異常宜早不宜遲。
-
切忌抓了,又把異常吞掉,不留下一絲痕跡
抓住異常,打行日志完事兒,不是一個好習慣。
-
切忌抓異常了將異常狀態流和業務狀態流混在一起,這樣你算是徹底拋棄了Java的異常機制