深入理解Java異常的使用場景
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的異常機制