关于 Java
全局异常处理,网上一搜都是说 SpringMVC
的全局异常处理。确实,使用 Spring Boot
开发也好,使用 SSM
也好,都可以使用 SpringMVC
的全局异常处理,也是最好不过,因为出现异常我们也要响应数据给前端。话说,关于 SpringMVC
的全局异常处理,你知道原理了吗?其实可以一句话概括,任何请求都先经过 DispatchServlet
。
如果应用不是一个 SpringMVC
应用呢?可能很多人都不知道, Java
自己就提供有全局异常处理。这个我还是从我的前领导那里听来的。但这个不适用于接口的全局异常处理。本篇将介绍如何使用 Java
提供的全局异常处理,以及分析一点 hotspot
虚拟机的源码,让大家了解虚拟机是如何将异常交给全局异常处理器处理的。
在 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
。
@FunctionalInterface public 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
便是异常对象。
看完本篇,我相信大家都已经了解了 Java
默认异常处理器是怎么被调用的了。那么,学习这个我们工作中是否能够用得到,那就看大家各自的发挥了。