給出以下異常信息:java
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的異常信息嗎?你能說清楚異常拋出過程當中的事件順序嗎?git
上述異常信息在由一個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();
}
}
}複製代碼
此次我複製了完整的文件內容,使文章中的代碼行號和實際行號一一對應。github
根據上述異常信息,異常拋出過程當中的事件順序是:bash
那麼,如何閱讀異常信息呢?有幾點你須要認識清楚:服務器
i+1
個異常是第i
個異常被拋出的緣由cause
,以「Caused by」開頭。如今,回過頭再去閱讀示例的異常信息,是否是至關簡單?app
爲了幫助理解,我儘量通俗易懂的描述了異常信息的結構和組成元素,可能會引入一些紕漏。閱讀異常信息是Java程序猿的基本技能,但願你能內化它,忘掉這些冗長的描述。框架
若是還不理解,建議你親自追蹤一次異常的建立和打印過程,使用示例代碼便可,它很簡單但足夠。難點在於異常是JVM提供的機制,你須要瞭解JVM的實現;且底層調用了不少native方法,而追蹤native代碼沒有那麼方便。ide
示例的異常信息中,異常名、細節信息、路徑三個元素都有,可是,因爲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 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。