原创 吴就业 133 0 2020-03-09
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/cef242ada38e4b71aa4f74a425602579
作者:吴就业
链接:https://wujiuye.com/article/cef242ada38e4b71aa4f74a425602579
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
本篇文章写于2020年03月09日,从公众号同步过来(博客搬家),本篇为原创文章。
关于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
默认异常处理器是怎么被调用的了。那么,学习这个我们工作中是否能够用得到,那就看大家各自的发挥了。
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
本篇介绍是什么原因导致的`mybatis-plus`分页插件性能下降,以及如何通过使用`JsqlParser`这个开源的`sql`解析工具包与`mybatis-plus`提供的自定义`sql`优化器功能,自己实现高性能的分页插件。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。