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

 

原文:https://www.jianshu.com/p/15872cba211d

Java異常控制機制又被稱爲「違例控制機制」。
捕獲程序錯誤最理想的時機是在編譯階段,這樣能夠完全避免錯誤的代碼運行。但並不是全部的錯誤都能在編譯期間偵測到,有些問題必須在運行期間解決。java

錯誤在運行期間發生時,咱們可能不知道具體應該怎樣解決,但咱們清楚此時不能無論不顧地繼續執行下去。此時應該作的事情是:程序員

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

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

  1. 異常產生
    首先程序引擎須要可以獲知異常的產生。Java中預置了一系列基本的異常條件,如數組下標越界、空指針、被零除等等,這些異常是由JVM自動產生的(也被稱爲運行時異常,見後);另外一部分異常則是由Java代碼(多是JDK的代碼或開發人員本身編寫的代碼)產生的(也被稱爲checked異常,見後)。
    異常產生便是異常對象的實例化,該對象的類型一般就說明了異常條件的類型,實例化的異常對象中還會包含對異常條件的補充說明(message),以及異常發生時的線程調用棧信息(stacktrace)。
    在這個環節中,JAVA完成了對錯誤的描述,包括錯誤發生的時間、錯誤的類型(即異常對象的Class)、對錯誤的描述(message)和錯誤發生的位置(stacktrace)。編程

  2. 異常拋出
    異常拋出是JAVA程序流中的一種特殊流程,當異常產生後,JVM會中止繼續執行後面的代碼,並將異常對象拋出。拋出的異常對象會進入調用棧的上一層,若是異常對象沒有被捕獲,它會沿着調用棧的順序逐層向上拋出,直至調用棧爲空,此時該線程的運行也就完全終止了。
    異常的拋出解決了當前做用域可能不具有處理異常所需的信息的問題,將異常對象在調用棧中逐級向上傳遞,直至有能力處理異常的做用域將其捕獲。api

  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則是「應用程序應該嘗試捕捉處理的問題」。spa

咱們從幾個例子看一下:線程

  • 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官方將其定義爲「應用程序不該嘗試捕捉處理的嚴重問題」。3d


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會所有被吃掉。

正確的寫法:

 
圖片.png
相關文章
相關標籤/搜索