科技

你真的會閱讀 Java 的異常資訊嗎

給出如下異常資訊:

學這麼多年Java,你真的會閱讀Java的異常資訊嗎?你能說清楚異常丟擲過程中的事件順序嗎?

需要內化的內容

寫一個demo測試

上述異常資訊在由一個demo產生:

這次我複製了完整的檔案內容,使文章中的程式碼行號和實際行號一一對應。

根據上述異常資訊,異常丟擲過程中的事件順序是:

在Test.java的第10行,丟擲了一個IOExceotion(“level 1 exception”) e1異常e1被逐層向外丟擲,直到在Test.java的第15行被捕獲在Test.java的第17行,根據捕獲的異常e1,丟擲了一個RuntimeException(“level 2 exception”, e1) e2異常e2被逐層向外丟擲,直到在Test.java的第24行被捕獲後續沒有其他異常資訊,經過必要的框架後,由程式自動或使用者主動呼叫了e2.printStackTrace()方法如何閱讀異常資訊

那麼,如何閱讀異常資訊呢?有幾點你需要認識清楚:

異常棧以FILO的順序列印,位於列印內容最下方的異常最早被丟擲,逐漸導致上方異常被丟擲。位於列印內容最上方的異常最晚被丟擲,且沒有再被捕獲。從上到下數,第i+1個異常是第i個異常被丟擲的原因cause,以“Caused by”開頭。異常棧中每個異常都由異常名+細節資訊+路徑組成。異常名從行首開始(或緊隨”Caused by”),緊接著是細節資訊(為增強可讀性,需要提供恰當的細節資訊),從下一行開始,跳過一個製表符,就是路徑中的一個位置,一行一個位置。路徑以FIFO的順序列印,位於列印內容最上方的位置最早被該異常經過,逐層向外丟擲。最早經過的位置即是異常被丟擲的位置,逆向debug時可從此處開始;後續位置一般是方法呼叫的入口,JVM捕獲異常時可以從方法棧中得到。對於cause,其可列印的路徑截止到被包裝進下一個異常之前,之後列印“… 6 more”,表示cause作為被包裝異常,在這之後還逐層向外經過了6個位置,但這些位置與包裝異常的路徑重複,所以在此處省略,而在包裝異常的路徑中列印。“… 6 more”的資訊不重要,可以忽略。現在,回過頭再去閱讀示例的異常資訊,是不是相當簡單?

為了幫助理解,我儘可能通俗易懂的描述了異常資訊的結構和組成元素,可能會引入一些紕漏。閱讀異常資訊是Java程式猿的基本技能,希望你能內化它,忘掉這些冗長的描述。

如果還不理解,建議你親自追蹤一次異常的建立和列印過程,使用示例程式碼即可,它很簡單但足夠。難點在於異常是JVM提供的機制,你需要了解JVM的實現;且底層呼叫了很多native方法,而追蹤native程式碼沒有那麼方便。

擴充套件

為什麼有時我在日誌中只看到異常名”java.lang.NullPointerException”,卻沒有異常棧

示例的異常資訊中,異常名、細節資訊、路徑三個元素都有,但是,由於JVM的優化,細節資訊和路徑可能會被省略。

這經常發生於伺服器應用的日誌中,由於相同異常已被列印多次,如果繼續列印相同異常,JVM會省略掉細節資訊和路徑佇列,向前翻閱即可找到完整的異常資訊。

猴哥之前使用Yarn的Timeline Server時遇到過該問題。你能體會那種感覺嗎?臥槽,為什麼只有異常名沒有異常棧?沒有異常棧怎麼老子怎麼知道哪裡丟擲的異常?線上服務老子又不能停,全靠日誌了啊喂!

如何在異常類中新增成員變數

為了恰當的表達一個異常,我們有時候需要自定義異常,並新增一些成員變數,列印異常棧時,自動補充列印必要的資訊。

追蹤列印異常棧的程式碼:

暫不關心同步問題,可知,列印異常名和細節資訊的程式碼為:

s.println(this);

JVM在執行期通過動態繫結實現this引用上的多型呼叫。繼續追蹤的話,最終會呼叫this例項的toString()方法。所有異常的最低公共祖先類是Throwable類,它提供了預設的toString()實現,大部分常見的異常類都沒有覆寫這個實現,我們自定義的異常也可以直接繼承這個實現:

顯然,預設實現的列印格式就是示例的異常資訊格式:異常名(全限定名)+細節資訊。detailMessage由使用者建立異常時設定,因此,如果有自定義的成員變數,我們通常在toString()方法中插入這個變數。參考com.sun.javaws.exceptions包中的BadFieldException,看看它如何插入自定義的成員變數field和value:

嚴格的說,BadFieldException的toString中並沒有直接插入field成員變數。不過這不影響我們理解,感興趣的讀者可自行翻閱原始碼。

總結

根據異常資訊debug是程式設計師的基本技能,這裡圍繞異常資訊的閱讀和列印過程作了初步探索,後續還會整理一下常用的異常類。

Java相當完備的異常處理機制是一把雙刃劍,用好它能增強程式碼的可讀性和魯棒性,用不好則會讓程式碼變的更加不可控。例如,在空指標上呼叫成員方法,執行期會丟擲異常,這是很自然的——但是,是不可控的等待它在某個時刻某個位置丟擲異常(實際上還是“確定”的,但對於debug來說是“不確定”的),還是可控的在進入方法伊始就檢查並主動丟擲異常呢?進一步的,哪些異常應該被即刻處理,哪些應該繼續拋到外層呢?拋往外層時,何時需要封裝異常呢?看看String#toLowerCase(),看看ProcessBuilder#start(),體會一下。

Reference:科技日報

看更多!請加入我們的粉絲團

轉載請附文章網址

不可錯過的話題