你真的會閱讀Java的異常信息嗎?

給出以下異常信息: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測試

上述異常信息在由一個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

  1. 在Test.java的第10行,拋出了一個IOExceotion("level 1 exception") e1
  2. 異常e1被逐層向外拋出,直到在Test.java的第15行被捕獲
  3. 在Test.java的第17行,根據捕獲的異常e1,拋出了一個RuntimeException("level 2 exception", e1) e2
  4. 異常e2被逐層向外拋出,直到在Test.java的第24行被捕獲
  5. 後續沒有其餘異常信息,通過必要的框架後,由程序自動或用戶主動調用了e2.printStackTrace()方法

如何閱讀異常信息

那麼,如何閱讀異常信息呢?有幾點你須要認識清楚:服務器

  • 異常棧以FILO的順序打印,位於打印內容最下方的異常最先被拋出,逐漸致使上方異常被拋出。位於打印內容最上方的異常最晚被拋出,且沒有再被捕獲。從上到下數,第i+1個異常是第i個異常被拋出的緣由cause,以「Caused by」開頭。
  • 異常棧中每一個異常都由異常名+細節信息+路徑組成。異常名從行首開始(或緊隨"Caused by"),緊接着是細節信息(爲加強可讀性,須要提供恰當的細節信息),從下一行開始,跳過一個製表符,就是路徑中的一個位置,一行一個位置。
  • 路徑以FIFO的順序打印,位於打印內容最上方的位置最先被該異常通過,逐層向外拋出。最先通過的位置便是異常被拋出的位置,逆向debug時可今後處開始;後續位置通常是方法調用的入口,JVM捕獲異常時能夠從方法棧中獲得。對於cause,其可打印的路徑截止到被包裝進下一個異常以前,以後打印「... 6 more」,表示cause做爲被包裝異常,在這以後還逐層向外通過了6個位置,但這些位置與包裝異常的路徑重複,因此在此處省略,而在包裝異常的路徑中打印。「... 6 more」的信息不重要,能夠忽略。

如今,回過頭再去閱讀示例的異常信息,是否是至關簡單?app

爲了幫助理解,我儘量通俗易懂的描述了異常信息的結構和組成元素,可能會引入一些紕漏。閱讀異常信息是Java程序猿的基本技能,但願你能內化它,忘掉這些冗長的描述。框架

若是還不理解,建議你親自追蹤一次異常的建立和打印過程,使用示例代碼便可,它很簡單但足夠。難點在於異常是JVM提供的機制,你須要瞭解JVM的實現;且底層調用了不少native方法,而追蹤native代碼沒有那麼方便。ide

擴展

爲何有時我在日誌中只看到異常名"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 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索