1. 程式人生 > >【轉載】Java異常控制機制和異常處理原則

【轉載】Java異常控制機制和異常處理原則

轉載自Java異常控制機制和異常處理原則

Java異常控制機制又被稱為“違例控制機制”。
捕獲程式錯誤最理想的時機是在編譯階段,這樣可以徹底避免錯誤的程式碼執行。但並非所有的錯誤都能在編譯期間偵測到,有些問題必須在執行期間解決。

錯誤在執行期間發生時,我們可能不知道具體應該怎樣解決,但我們清楚此時不能不管不顧地繼續執行下去。此時應該做的事情是:

  • 暫停程式的執行
  • 指出何時、何地發生了什麼樣的錯誤
  • 可能的話應處理此錯誤並恢復程式的執行

Java異常控制機制的作用流程:

  1. 異常產生
    首先程式引擎需要能夠獲知異常的產生。Java中預置了一系列基本的異常條件,如陣列下標越界、空指標、被零除等等,這些異常是由JVM自動產生的(也被稱為執行時異常,見後);另一部分異常則是由Java程式碼(可能是JDK的程式碼或開發人員自己編寫的程式碼)產生的(也被稱為checked異常,見後)。
    異常產生即是異常物件的例項化,該物件的型別通常就說明了異常條件的型別,例項化的異常物件中還會包含對異常條件的補充說明(message),以及異常發生時的執行緒呼叫棧資訊(stacktrace)。
    在這個環節中,JAVA完成了對錯誤的描述,包括錯誤發生的時間、錯誤的型別(即異常物件的Class)、對錯誤的描述(message)和錯誤發生的位置(stacktrace)。

  2. 異常丟擲
    異常丟擲是JAVA程式流中的一種特殊流程,當異常產生後,JVM會停止繼續執行後面的程式碼,並將異常物件丟擲。丟擲的異常物件會進入呼叫棧的上一層,如果異常物件沒有被捕獲,它會沿著呼叫棧的順序逐層向上丟擲,直至呼叫棧為空,此時該執行緒的執行也就徹底終止了。
    異常的丟擲解決了當前作用域可能不具備處理異常所需的資訊的問題,將異常物件在呼叫棧中逐級向上傳遞,直至有能力處理異常的作用域將其捕獲。

  3. 異常捕獲
    在異常物件逐級向上丟擲的過程中,如果呼叫棧中某一層有捕獲該型別異常的邏輯,該異常物件便會被捕捉,異常被捕獲後JVM會終止丟擲異常物件的過程。

  4. 異常處理
    當異常物件被捕獲後,JVM會執行捕獲後的處理邏輯(處理邏輯是由程式設計師編寫的)。當處理邏輯執行完成後,JVM會繼續執行捕獲了異常的作用域中接下來的程式碼(除非異常處理邏輯中將該異常繼續丟擲,或異常處理邏輯中產生了新的異常)。


try-catch-finally

前文所述的異常控制流程,在JAVA程式中以try-catch-finally結構實現:

  圖片.png
  1. try塊也被稱為“警戒區”,try塊包裹的程式碼在執行過程如果產生異常,或其呼叫棧的下層中產生了異常並被拋至本層,則會被與此try塊關聯的catch命令嘗試捕獲。若異常產生於警戒區之外,則會直接向上層丟擲。
  2. catch命令後的括號內指定希望捕捉的異常物件型別(可以指定多個),如果產生或被拋至此層的異常物件是catch指定的異常型別(或其子類),則異常物件會被捕捉。上例中,所有Exception物件及其子類的物件在此處均會被捕獲。
  3. 被捕獲後,JVM會執行catch塊中的程式碼,catch塊中的程式碼能夠訪問被捕捉到的異常物件(即上例中的Exception e)。
    catch塊中的程式碼仍然有可能產生異常,所以也可以在catch塊中插入try-catch-finally。
  4. finally塊為可選塊,如果有,則無論是否有異常被丟擲,JVM都會在try-catch塊執行完成後執行finally塊中的程式碼。

Exception與Error

前文所述的Java異常控制機制實際上並不僅對“異常”起作用。除了我們所說的異常(Exception)能夠被產生、丟擲和捕捉之外,還有另一種型別“錯誤(Error)”。
Java中,Throwable是所有可以被丟擲並捕獲的類的父類。Throwable有兩大子類,分別是Exception和Error。
Java官方並沒有給出Error和Exception的嚴格定義,而是將Error描述為“應用程式不應嘗試捕捉處理的嚴重問題”,Exception則是“應用程式應該嘗試捕捉處理的問題”。

我們從幾個例子看一下:

  • NoClassDefFoundError:JVM的ClassLoader在嘗試載入某個類,但該類在Classpath中並不存在時會產生的錯誤。例如a.jar依賴b.jar中的某個類,如果我們使用編譯完成的a.jar時並沒有引入b.jar,編譯器並不會發現問題(因為a.jar已經完成了編譯,需要編譯的程式碼中只使用了a.jar中的api,並沒有直接使用b.jar),但在執行時JVM找不到b.jar中被a所依賴的類,便會發生錯誤。
  • UnsupportedClassVersionError:當JVM嘗試載入一個class但發現該class的版本並不被支援時產生的錯誤。例如我們使用JDK1.8開發並編譯一個類,但在JDK1.7的環境中執行時,便會發生此錯誤
  • OutOfMemoryError:當JVM記憶體不足,無法為一個物件分配記憶體時發生的錯誤,例如堆區記憶體溢位、Perm區記憶體溢位等。
  • StackOverFlowError:當程式的遞迴呼叫過深,導致執行緒呼叫棧溢位時發生的錯誤。
  • NoSuchFieldError/NoSuchMethodError:當JVM試圖訪問某個成員屬性或某個方法時,發現目標不存在。一般都是由於class資訊在執行時被改變導致的,多見於使用反射時。

通過上面的例子能夠看出,Error一般都與程式本身的直接關係不大,更多是由於環境導致的問題。而且Error發生後通常程式都沒有再繼續執行下去的可能性,所以Java官方將其定義為“應用程式不應嘗試捕捉處理的嚴重問題”。


Exception的分類

Java將Exception分為兩類,checked異常和unchecked異常,也被稱為非執行時異常和執行時(runtime)異常。
RuntimeException是Exception的一個子類,RuntimeException的子類都屬於unchecked異常(也就是執行時異常),其他所有的Exception都是checked異常(也就是非執行時異常)。

這兩種異常的區別從字面上即可理解,checked代表“必須被check”,而unchecked代表“無須被check”:
Java要求checked異常必須被在程式碼編寫階段就呼叫者瞭解,unchecked異常則不用。如果一個方法中有可能產生checked異常,則Java編譯器會要求該方法定義中必須加入throws定義,明確說明該方法可能會丟擲某類checked異常。如下圖:

  圖片.png

foo方法可能產生IOException(這是一種checked異常),所以bar方法在呼叫foo時,編譯器會提示錯誤。此時可以在bar方法的定義行中加入throws:

public void bar() throws IOException 

也可以在bar方法內將IOException捕獲處理:

  圖片.png

另一個理解checked異常與unchecked異常區別的角度是:所有由JVM自動生成的異常都是unchecked異常,反之,由java程式主動生成的異常是checked異常。
例如:

  圖片.png

上圖中f.createNewFile()方法可能會產生checked異常IOException,我們看看File類的原始碼:

  這裡寫圖片描述

可以看到紅框處,IOException異常是在程式碼中被主動丟擲的,凡是這樣在程式碼中主動丟擲的異常,都是checked異常。

相應地,unchecked異常是JVM在執行時自動產生的,例如下圖的方法,只要傳入的引數b等於0,就會在執行時自動產生ArithmeticException:

  圖片.png   圖片.png

程式碼中永遠不需要這樣寫:

  圖片.png

異常處理的原則

異常處理的原則主要有三個:

  • 具體明確
  • 提早丟擲
  • 延遲捕獲

具體明確:
指丟擲的異常應能通過異常類名和message準確說明異常的型別和產生異常的原因。

我們通過例子來看:

程式碼1:

  圖片.png

程式碼2:

  圖片.png

這兩段程式碼的處理邏輯是類似的,均是在入參input1或input2為null或空串時丟擲異常,但只有第二段符合“具體明確”的標準:
首先,第二段程式碼通過異常型別【IllegalArgumentException】明確了異常是由於傳入了不合法的引數導致的;其次,在message中說明了具體是哪個引數不合法,為什麼不合法。這樣不僅能夠在查閱日誌時快速知曉異常產生的原因,也讓上層的程式能夠針對IllegalArgumentException這一特定型別的異常進行有針對性的捕捉和處理。
相比之下,第一段程式碼中丟擲的異常就不夠具體明確,異常型別Exception不具有說明性質,異常message也不夠明確,上層程式難以處理,閱讀日誌時也難以快速定位。

提早丟擲:

指應儘可能早的發現並丟擲異常,便於精確定位問題。

同樣通過例子來看:

程式碼1:

  圖片.png

程式碼2:

  圖片.png

在傳入的filename為null時,這兩段程式碼都會丟擲異常,第一段程式碼丟擲的異常是:

  圖片.png

第二段程式碼丟擲的異常是:

  圖片.png

第一段程式碼丟擲的異常是在標準Java類庫【InputFileStream】中丟擲的,這首先就提升了問題定位的難度,不過幸好stacktrace中也打印出了前面的呼叫鏈,我們可以在標準類庫的呼叫者身上查詢問題(可以定位到Test.java的第38行)。
同時NullPointerException是Java中資訊量最少的(卻也是最常遭遇且讓人崩潰的)異常。它壓根不提我們最關心的事情:到底哪裡是null。在稍微複雜一些的場景中(如一行程式碼中有多處都可能導致NullPointerException)會讓人更加崩潰。

而相比之下第二段程式碼對filename提前進行了校驗,並以IllegalArgumentException的形式丟擲,這樣在第一段程式碼中遇到的兩個問題都可以得到解決,這便是提早丟擲的好處。

延遲捕獲:

指異常的捕獲和處理應儘可能延遲,讓掌握更多資訊的作用域來處理異常。

程式碼1:

  圖片.png

上面的程式碼中,readSomeFile方法將new FileInputStream處有可能產生的FileNotFoundException捕獲,並將異常資訊記錄到了日誌中。
這麼做看起來似乎沒什麼問題,但readSomeFile這個方法有可能是一個通用的底層方法,會在各種業務場景下被呼叫,不同的業務場景下,發生FileNotFoundException時的處理策略可能不一樣(例如某些場景要求記錄異常並告警,某些場景會使用其他檔名重試),但readSomeFile方法並不知道自己所處的業務場景是什麼樣的,這一資訊只有更上層的作用域才瞭解,所以在方法內部直接捕獲並處理異常的做法就顯得有問題了,程式將無法通過甄別業務場景來執行不同的異常處理邏輯。

程式碼2:

  圖片.png

第二段程式碼看起來反而更加簡單了,沒有對FileNotFoundException加以處理,而是直接在方法定義中將其丟擲。然而在上面所述的場景下,這種處理方式反而是正確的。將異常丟擲交由掌握了足夠多資訊的上層呼叫者捕獲,這樣就可以根據異常產生所處的具體業務流程來進行不同的處理。

例如我們可以在一個業務邏輯中這樣處理:

  圖片.png

同時在另一個業務邏輯中這樣處理:

  圖片.png

其他重要原則

  1. 不要讓異常逃掉
    當一個異常在整個呼叫棧中的任意一層都沒有被捕獲,這個異常就“逃掉”了。這對於任何程式來說都是一個災難性的事件。
    對於B/S系統,從請求處理執行緒中逃掉的異常很可能會被B/S框架(如Struts/SpringMVC等)捕捉到。如果沒有正確配置,這些逃掉的異常很可能就被框架“吃掉”了,即框架捕獲了從業務程式碼層丟擲的異常,且沒有記錄或沒有完整記錄異常資訊。這樣的異常來無影去無蹤,完全無跡可尋,堪稱程式設計師的大敵。
    某些情況下,異常會被拋到中介軟體或容器(Tomcat/Jboss/Weblogic/Websphere等)層(可能是沒有使用B/S框架或B/S框架沒有“吃掉”異常)。被中介軟體或容器捕獲到的異常,一般情況下會被記錄在中介軟體或容器自己的日誌中(也有可能不會記),但問題在於,這種情況下,使用者會看到中介軟體或容器提供的錯誤頁,這些錯誤頁基本沒有使用者友好型可言,而且有可能會把異常堆疊的資訊直接顯示在頁面上,在開放性的系統中,暴露堆疊資訊極有可能引發嚴重的安全問題。
    而在後臺程序中,如果異常逃掉了,將會導致執行緒的退出。如果沒有守護執行緒及時補充異常退出的執行緒,那麼將有可能發生整個程序因為異常而中止的災難性後果。
    所以說,在程式設計時應絕對避免異常“逃逸”的情況,對於B/S系統來說,我們可以在每個Action中都加入try-catch塊,捕獲所有Exception,也可以利用B/S框架的特性來實現從Action層丟擲的異常的統一處理(如Struts2和SpringMVC都有的攔截器機制)。對於後臺程序來說,可以利用try-catch塊避免異常導致執行緒中止,也可以通過新增守護執行緒來及時補充因異常而退出的執行緒,同時還應使用Thread.setDefaultUncaughtExceptionHandler來確保未捕獲異常的正確記錄。

  2. 正確記錄異常資訊
    即在異常的stacktrace資訊完整、未缺失的基礎上,確保異常的stacktrace被正確記錄到日誌中

錯誤的做法:

  圖片.png

上面的5種處理全都是錯誤的,前兩種將異常資訊輸出到了控制檯而不是日誌檔案中。後三種錯誤的使用了log4j的error方法,均沒有正確記錄異常的stacktrace

正確的方法:

  圖片.png

注意應使用正確的error方法,傳入兩個引數,引數1是對異常的附加描述,引數2是未被篡改過的異常物件
在某些情況下,可能需要在處理異常後繼續丟擲,讓上層捕獲後繼續處理,在這種情況下,需要注意丟擲的異常物件未被篡改。

錯誤的:

  圖片.png

如果像上圖這樣寫的話,下層的異常stacktrace會全部被吃掉。

正確的寫法:

 

作者:kelgon
連結:https://www.jianshu.com/p/15872cba211d
來源:簡書