給出如下異常資訊:
java.lang.RuntimeException: level 2 exception
at com.msh.demo.exceptionStack.Test.fun2(Test.java:17)
at com.msh.demo.exceptionStack.Test.main(Test.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.io.IOException: level 1 exception
at com.msh.demo.exceptionStack.Test.fun1(Test.java:10)
at com.msh.demo.exceptionStack.Test.fun2(Test.java:15)
... 6 more複製程式碼
學這麼多年Java,你真的會閱讀Java的異常資訊嗎?你能說清楚異常丟擲過程中的事件順序嗎?
需要內化的內容
寫一個demo測試
上述異常資訊在由一個demo產生:
package com.msh.demo.exceptionStack;
import java.io.IOException;
/**
* Created by monkeysayhi on 2017/10/1.
*/
public class Test {
private void fun1() throws IOException {
throw new IOException("level 1 exception");
}
private void fun2() {
try {
fun1();
} catch (IOException e) {
throw new RuntimeException("level 2 exception", e);
}
}
public static void main(String[] args) {
try {
new Test().fun2();
} catch (Exception e) {
e.printStackTrace();
}
}
}複製程式碼
這次我複製了完整的檔案內容,使文章中的程式碼行號和實際行號一一對應。
根據上述異常資訊,異常丟擲過程中的事件順序是:
- 在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時遇到過該問題。你能體會那種感覺嗎?臥槽,為什麼只有異常名沒有異常棧?沒有異常棧怎麼老子怎麼知道哪裡丟擲的異常?線上服務老子又不能停,全靠日誌了啊喂!
網上有不少相同的case,比如NullPointerException丟失異常堆疊資訊,讀者可以參照這個連結實驗一下。
如何在異常類中新增成員變數
為了恰當的表達一個異常,我們有時候需要自定義異常,並新增一些成員變數,列印異常棧時,自動補充列印必要的資訊。
追蹤列印異常棧的程式碼:
...
public void printStackTrace() {
printStackTrace(System.err);
}
...
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
...
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu =
Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
dejaVu.add(this);
synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);
// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
...複製程式碼
暫不關心同步問題,可知,列印異常名和細節資訊的程式碼為:
s.println(this);複製程式碼
JVM在執行期通過動態繫結實現this引用上的多型呼叫。繼續追蹤的話,最終會呼叫this例項的toString()方法。所有異常的最低公共祖先類是Throwable類,它提供了預設的toString()實現,大部分常見的異常類都沒有覆寫這個實現,我們自定義的異常也可以直接繼承這個實現:
...
public String toString() {
String s = getClass().getName();
String message = getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}
...
public String getLocalizedMessage() {
return getMessage();
}
...
public String getMessage() {
return detailMessage;
}
...複製程式碼
顯然,預設實現的列印格式就是示例的異常資訊格式:異常名(全限定名)+細節資訊。detailMessage由使用者建立異常時設定,因此,如果有自定義的成員變數,我們通常在toString()方法中插入這個變數。參考com.sun.javaws.exceptions
包中的BadFieldException
,看看它如何插入自定義的成員變數field和value:
public String toString() {
return this.getValue().equals("https")?"BadFieldException[ " + this.getRealMessage() + "]":"BadFieldException[ " + this.getField() + "," + this.getValue() + "]";
}複製程式碼
嚴格的說,
BadFieldException
的toString中並沒有直接插入field成員變數。不過這不影響我們理解,感興趣的讀者可自行翻閱原始碼。
總結
根據異常資訊debug是程式設計師的基本技能,這裡圍繞異常資訊的閱讀和列印過程作了初步探索,後續還會整理一下常用的異常類,結合程式猿應該記住的幾條基本規則,更好的理解如何用異常幫助我們寫出clean code。
Java相當完備的異常處理機制是一把雙刃劍,用好它能增強程式碼的可讀性和魯棒性,用不好則會讓程式碼變的更加不可控。例如,在空指標上呼叫成員方法,執行期會丟擲異常,這是很自然的——但是,是不可控的等待它在某個時刻某個位置丟擲異常(實際上還是“確定”的,但對於debug來說是“不確定”的),還是可控的在進入方法伊始就檢查並主動丟擲異常呢?進一步的,哪些異常應該被即刻處理,哪些應該繼續拋到外層呢?拋往外層時,何時需要封裝異常呢?看看String#toLowerCase(),看看ProcessBuilder#start(),體會一下。
本文連結:你真的會閱讀Java的異常資訊嗎?
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。