天天看點

Java全局異常處理,你不知道的騷操作(含hotspot源碼分析)

關于Java全局異常處理,網上一搜都是說SpringMVC的全局異常處理。确實,使用Spring Boot開發也好,使用SSM也好,都可以使用SpringMVC的全局異常處理,也是最好不過,因為出現異常我們也要響應資料給前端。話說,關于SpringMVC的全局異常處理,你知道原理了嗎?其實可以一句話概括,任何請求都先經過DispatchServlet。

如果應用不是一個SpringMVC應用呢?可能很多人都不知道,Java自己就提供有全局異常處理。這個我還是從我的前上司那裡聽來的。但這個不适用于接口的全局異常處理。本篇将介紹如何使用Java提供的全局異常處理,以及分析一點hotspot虛拟機的源碼,讓大家了解虛拟機是如何将異常交給全局異常處理器處理的。

全局異常處理Demo

在main方法中設定全局異常處理器DefaultUncaughtExceptionHandler,從名字中也可以看出,這是用于處理未捕獲的異常的。不一定是從main方法中設定,在spring應用中,可以監聽spring初始化完成事件,再設定。(如果是web應用,還是使用SpringMVC的全局異常處理。)

public static void main(String[] args) throws InterruptedException {
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("這裡是全局異常處理 ====> " + t.getId() + "==> "+e.getLocalizedMessage());
            }
        });
}複制代碼      

有朋友可能會覺得奇怪了,為什麼是設定在Thread裡面。雖然是設定在Thread裡面,但這是一個靜态變量。

要知道,我們所寫的代碼都是線上程裡面跑的。一個線程對應一個Java虛拟機棧,異常是在棧桢中發生的(即方法)。在調用發生異常時,棧桢出棧,異常一層層往上抛出,并寫入調用棧資訊。在整個調用棧中,如果都沒有方法捕獲異常,那麼Java虛拟機将從目前線程的Thread對象中擷取一個異常處理器,如果有,則交給異常處理器處理。走到這一步,意味着線程即将退出,這也是我從hotspot源碼中尋找入口的依據。

當然,如果是設定了針對某個線程的異常處理器,則該線程發現未捕獲異常時,會使用該線程設定的異常處理器,否則會使用全局預設的。這裡沒懂沒關系,後面會詳細分析。

我們接着把例子看完。在main方法中建立多個線程,并線上程的run方法中抛出異常。

private static class TaskThread extends Thread {    @Override
    public void run() {       throw new NullPointerException("thread-" + Thread.currentThread().getId() + " Exception");
    }
}/**
 * 在main方法中調用startThread(),
 */public static void startThread(){    for (int i = 0; i < 10; i++) {        new TaskThread().start();
    }    // 不讓主線程退出
    System.in.read();
}複制代碼      

程式運作結果:

這裡是全局異常處理 ====> 13==> thread-13 Exception
這裡是全局異常處理 ====> 16==> thread-16 Exception
這裡是全局異常處理 ====> 15==> thread-15 Exception
........複制代碼      

前面提到,我們還可以針對某個線程設定單獨的異常處理器,且優先級會高于全局預設的。如果為某個線程單獨設定異常處理器,那麼就這個線程而言,預設的全局異常處理器将不起作用。我們來修改一下前面例子的startThread方法,驗證一下,其它不變。

/**
 * 在main方法中調用startThread(),
 */public static void startThread(){    for (int i = 0; i < 10; i++) {
        Thread thread = new TaskThread();        if (i == 0) {
            thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("這是為目前線程設定的異步處理器。===> " + t.getId());
                }
            });
        }
        thread.start();
    }    // 不讓主線程退出
    System.in.read();
}複制代碼      

程式輸出結果如下:

這是為目前線程設定的異步處理器。===> 13
這裡是全局異常處理 ====> 16==> thread-16 Exception
這裡是全局異常處理 ====> 15==> thread-15 Exception
.......複制代碼      

很顯然,id等于13的線程走了單獨的異常處理器,而其它線程則走全局預設異常處理器。有朋友可能會好奇,為什麼線程id從13開始,其實我在以前的文章也提到過,隻是忘記是哪篇了。id為0的是mian線程,然後接着就是虛拟機的線程,以及gc垃圾回收的線程,由于我電腦cpu是9代i7六核十二線程,是以gc線程數比較多。題外話就不扯太多了。

從源碼中尋找答案

Java的全局異常處理是從jdk1.5開始加入的新特性,我也不确定是不是1.5,注釋上寫的。先看下異常處理器UncaughtExceptionHandler。

@FunctionalInterfacepublic interface UncaughtExceptionHandler {    /**
      * 未捕獲異常處理
      */
    void uncaughtException(Thread t, Throwable e);
}複制代碼      

如果在uncaughtException方法中,比如寫日記記錄日常資訊,結果因為寫日記時發生IO異常,或者其它異常,不管是什麼異常,此方法抛出的異常都将會被Java虛拟機忽略,因為線程已經要結束退出了。

Thread中聲明了兩個UncaughtExceptionHandler類型的變量,一個是靜态變量。其中非靜态變量是針對目前線程起作用的,聲明為volatile原因是可能是其它線程調用設定的;另一個靜态變量就是全局預設的。

public class Thread implements Runnable {    // null unless explicitly set
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
}複制代碼      

我在看源碼的時候,是通過搜尋檢視哪個地方使用了這兩個UncaughtExceptionHandler,最後在dispatchUncaughtException方法上的注釋看到了關鍵資訊。當有未捕獲異常抛出時,java虛拟機會調用目前線程的Thread對象的dispatchUncaughtException方法。

/**
     * Dispatch an uncaught exception to the handler. This method is
     * intended to be called only by the JVM.
     */
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }複制代碼      

在dispatchUncaughtException方法中,調用了getUncaughtExceptionHandler方法擷取UncaughtExceptionHandler異常處理器對象,再把異常交給拿到的異常處理器去處理。

public UncaughtExceptionHandler getUncaughtExceptionHandler() {    return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group;
}複制代碼      

這裡非常奇怪,并沒有用到defaultUncaughtExceptionHandler這個靜态變量。如果目前線程對象沒有設定異常處理器,就傳回一個group。首先看到這,我們就能明白,為什麼針對某個線程設定的異常處理器會被優先使用。

而group其實是ThreadGroup對象。在Java中,每個線程都有一個所屬的線程組。在調用start方法時,會将目前線程加入一個線程組,而如果在建立Thread對象時,沒有傳入線程組ThreadGroup,則會擷取目前線程的線程組,可能就是main線程所屬的線程組了,是不是有點繞,自己看下源碼就很好了解了。

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
}複制代碼      

ThreadGroup實作了UncaughtExceptionHandler接口,也就說得通,為什麼是傳回一個group了,然後看ThreadGroup的uncaughtException方法。

public void uncaughtException(Thread t, Throwable e) {    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        ueh.uncaughtException(t, e);
    }
}複制代碼      

線程組還有父線程組,這個太繞,我們不理它。可以看到,ThreadGroup的uncaughtException方法中,會調用Thread.getDefaultUncaughtExceptionHandler();方法擷取設定的預設異常處理器,這便是我們設定的全局預設異常處理器。

其實細心看代碼,你會發現,ThreadGroup的uncaughtException注釋是1.0版本就已經存在了。然後我看hotspot源碼,發現它會相容舊版本,即Thread對象不存在dispatchUncaughtException方法時,是轉為調用ThreadGroup的uncaughtException方法的。

接下來我們就要看hotspot源碼了,看下hotspot是怎麼調用異常處理器處理異常的。不知道看到這,你是否還記得前面說的一句話,異常處理器會被調用,說明目前Java虛拟機棧上沒有一個棧桢去捕獲異常,也意味着目前線程即将退出。是以源碼的入口就是thread.cpp類的exit方法。下面我将以圖檔方式貼代碼了。

(源碼所在檔案:vm/runtime/thread.cpp)

c++的知識我就不說了。看圖中的紅框0,調用resolve_virtual_call方法擷取調用資訊,即CallInfo。傳遞的參數分别是CallInfo的指針(配置設定在c++線程棧上的)、目前線程對象Thread、線程Thread的Class類結構資訊Klass、dispatchUncaughtException的方法名、方法簽名等。

繼續看thread.cpp的exit方法,紅框1是判斷Thread是否存在dispatchUncaughtException方法,即前面說的相容舊版本的。如果存在,則調用目前線程的Thread對象的dispatchUncaughtException方法。

如果是jdk1.0,那麼不會走紅框1的代碼,而是走紅框2,擷取目前線程的ThreadGroup對象,調用它的uncaughtException方法。調用call_virtual方法去執行java代碼。參數1便是Thread對象,參數2便是異常對象。