概述
所有异常都是继承自java.lang.Throwable
类,Throwable有两个直接子类,Error和Exception。
Error用来表示程序底层或硬件有关的错误,这种错误和程序本身无关,如常见的NoClassDefFoundError。这种异常和程序本身无关,不需要检查,属于非受检异常。
Exception表示程序异常,可能是由于程序不严谨导致的,如NPE空指针异常。Exception下面派生RuntimeException和其他异常,其中RuntimeException表示运行时异常,也属于非受检异常。在编译时可以不需要强制检查的异常,不需要显式捕捉或抛出。
除Error和RuntimeException及派生类以外,其他异常都属于受检异常,如IOException、SQLException。在编译时强制进行检查的异常,这种异常需要显式的通过try/catch来捕捉,或通过throws抛出去,否则程序无法通过编译。设计强制检查的异常(受检异常),主要原因是考虑到程序的正确性、稳定性和可靠性。
try…catch…finally语句块
初中级笔试题可能会出现的知识点。这里直接给出一些结论:
- 受检异常,需要使用try来包裹可能会抛出异常的代码块,catch用于捕获异常并处理异常的代码块,常见的处理策略包括:打印错误日志、抛出自定义业务异常、释放资源、设置局部变量等
- 受检异常,还可以直接在方法签名上throws Exception,抛给方法调用者来处理。业务开发中,通常在Service层抛出自定义业务异常,然后在Controller层统一捕获异常并返回errCode和errMsg
- 不管有没有出现异常,finally仍然会执行
- 当try和catch中有return时,finally仍然会执行
- finally常用于释放IO资源、(分布式)锁的持有、
常见异常
初中级Java开发工程师面试中,经常会遇到的一个问题:说说你工作中经常遇到的异常?
面试官指的应该包括Exception和Error,回答问题时,不能只列举Exception。
简单列举Exception如下:
- NullPointerException:简称NPE。多少人栽在NPE上,多少资金损失是因为NPE。减少(无法杜绝)NPE的方法就是不停地空判断,或使用Optional类。可喜的是,升级到JDK 14以上版本,发生NPE时,JVM会打印具体哪个方法抛的空指针异常,避免同一行代码多个函数调用时无法判断具体是哪个函数抛异常的困扰,方便异常排查;
- ConcurrentModificationException:简称CME。当有多个迭代器同时遍历和修改Java集合(如ArrayList或HashMap),就有可能抛出CME异常。避免出现CME异常的措施如:加锁,使用CopyOnWriteArrayList,ConcurrentHashMap等集合。
- IndexOutOfBoundsException:索引越界,实现类有两个ArrayIndexOutOfBoundsException和StringIndexOutOfBoundsException。
- ClassCastException:类型转换失败。
- ClassNotFoundException:参考Java学习之NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError
简单列举Error如下:
- OutOfMemoryError:OOM,报错信息为:
java.lang.OutOfMemoryError:Java heap spacess
。遇到OOM时,需要先分清楚是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。 - StackOverflowError:栈溢出。栈溢出的原因:递归调用(如求解斐波那契数列问题时),大量循环或死循环,全局变量过多,数组、List、Map数据过大。
- NoClassDefFoundError:找不到类定义
- NoSuchMethodError:找不到方法
- NoSuchFieldError:找不到字段,上面这三种一般都是三方依赖冲突,通过使用maven工具来排查,如
mvn dependency:tree > tmp.txt
,或使用IDEA的Maven Helper插件
最佳实践
即所谓的Best Practice:
- 在finally中清理资源;
- 坚决要杜绝捕获异常后不做任何处理,即catch语句块为空;
- 捕获异常后的日志打印规范,如记录错误类和方法,记录详细的错误堆栈stacktrace方便排查问题;
- 使用Try-With-Resource语句,实现AutoCloseable接口的资源;
- 优先捕获特定的异常,其次再考虑其父类异常;
- 多使用自定义业务异常,一个异常对应有一个errCode和一个可读性良好的errMsg
进阶
异常表
在JVM中,异常处理不是由字节码指令(早期使用jsr、ret指令)来实现的,而是异常表。
如果一个方法定义有try-catch或try-finally,则会创建异常表,保存异常处理信息:
- 起始位置
- 结束位置
- 程序计数器记录的代码处理的偏移地址
- 被捕获的异常类在常量池中的索引
Exception table:
根据不同的type对应到不同的target上。在操作系统里,这个target也称为异常处理程序。就是特定问题出现时,去异常表查询这个问题对应的是哪个处理程序,然后去执行这个程序,完成异常处理。
面试可能会遇到的问题:finally为什么一定会执行?
查看编译后的字节码,可发现编译器把finally语句块里面的代码分别复制到try和catch语句块里面。
异常throw事件
jvmti中提供两个异常的事件,一个是包含throw和catch,一个是catch。选择功能多的那个方便一点。
通过方法签名,可以知道异常的线程,出异常的方法,行号,异常对象,catch的方法和行号。这里由于是触发的throw事件,所以如果只是new Exception的操作是不会触发事件的。有些代码通过创建Exception或Error来控制逻辑,只要不是throw,catch的这种逻辑,这里是检测不到的。如果异常只throw没有catch的话,catch的字段就是空的。
拓展
UncaughtExceptionHandler
在虚拟机中,当一个线程没有显式处理(即try catch)异常而抛出时,会将该异常事件报告给该线程对象的java.lang.Thread.UncaughtExceptionHandler
进行处理,如果线程没有设置UncaughtExceptionHandler,则默认会把异常栈信息输出到终端而使程序直接崩溃。所以如果想在线程意外崩溃时做一些处理就可以通过实现UncaughtExceptionHandler来满足需求。
线程崩溃时异常抛出的顺序:
- 先调用
Thread.getUncaughtExceptionHandler()
查看是否有自己对象特有的handler,如果有就直接处理 - 如果没有就调用ThreadGroup(UncaughtExceptionHandler的默认实现类)
- 如果ThreadGroup没啥特殊处理就会继续调用
Thread.getDefaultUncaughtExceptionHandler()
获取handler进行处理 - 如果默认handler也没有处理就直接执行正常的异常流程使程序崩溃。
ThreadGroup核心实现源码:
Spring MVC异常处理机制
参考Spring MVC系列之九大核心组件中的HandlerExceptionResolver部分。
Spring MVC全局异常处理
每个Controller层里的方法都需要进行异常捕获及处理,显然太繁琐且效率低。
自定义类并实现HandlerExceptionResolver接口并重写resolveException方法进行全局异常处理:
可以以不同的方式将异常结果返回给调用者(前端或其他后端服务)
- 返回ModelAndView
- 返回页面的地址
- 返回JSON
- 返回HTTP错误码
当然也可以使用下面Spring Boot全局异常处理方案。
Spring Boot全局异常处理
直接给出配置类:
Response是自定义的数据统一返回格式:
Dubbo处理异常
分布式调用链
参考