https://www.cnblogs.com/newber/p/15293184.html
今天我们来讲讲 Java 虚拟机的异常处理。
先讲异常吧。
在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。
其实很好理解,类比男女朋友或者是夫妻之间的关系。error相当于男女之间有人犯了原则性错误,这个错误导致双方关系破裂且不可逆转。exception呢,相当于男女双方有人犯了个可以被原谅的错误,还有机会可以挽回。和操作系统里中断和异常的区别差不多。
Exception 有一个特殊的子类 RuntimeException,也就是这个意思,用来表示“程序虽然无法继续执行,但是还能抢救一下”的情况。我们遇到过的的数组索引越界便是其中的一种。
RuntimeException 及其子类和 Error 属于 Java 里的非检查异常(unchecked exception)。其他所有异常则属于检查异常(checked exception)。
什么是检查异常呢?就是编译器要求你必须处理的异常。咱们运行代码之前呢,都是需要先进行编译这个环节。大家编程的时候应该遇到过,你写的某段代码,编译器要求你必须要对这段代码try…catch,或者throws exception,这就是检查异常,也就是说,你代码还没运行呢,编译器就会检查你的代码会不会出现异常,如果可能出现异常,那么它会要求你对可能出现的异常必须做出相应的处理,虚拟机会说你不处理异常,你和你的代码就都别想跑。
那么什么是非检查异常呢?对比之下,就比较好理解了。它指的是编译器不要求强制处置的异常,虽然你有可能出现错误,但是我虚拟机不会在编译的时候检查,编译器说这种东西我不管了,没必要,也不现实。为什么呢?因为非检查异常都包括这种NullPointerException,IndexOutOfBoundsException,VirtualMachineError等等,这些异常要咱们Java虚拟机去检查,真是太难为他了,因为写出这些异常的兄弟们,他们的心思有时候比女人还难猜,总会在你不经意间给你来一个异常,虚拟机说我搞不定,等运行之后我做完我该做的,其他的就由写这个代码的兄弟你自己再看看吧。因为虚拟机也是人写出来的,而咱们有时候写的代码根本不能看,咱们应该心知肚明啊。虚拟机也无法保证在编译期间就能找出来这些问题。而且啊,有些异常只能在运行时才能检查出来,比如空指针,堆溢出等。
接下来再聊一下异常实例的构造
它比较消耗虚拟机资源。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。讲到这,大家应该能联想到平时程序报错时控制台打印的一些内容了吧。
当然,在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧。我们在介绍 Lambda 表达式的时候会看到具体的例子。
那么这里就有个问题?既然异常实例的构造十分昂贵,我们是否可以缓存异常实例,在需要用到的时候直接抛出呢?从语法角度上来看,这是允许的。然而,该异常对应的栈轨迹并非 throw 语句的位置,而是新建异常的位置。
因此,这种做法可能会误导开发人员,使其定位到错误的位置。这也是为什么在实践中,我们往往选择抛出新建异常实例而不用缓存异常实例的原因。
最后聊一聊Java 虚拟机是如何捕获异常的?
Java字节码中,每个方法对应一个异常表。当程序触发异常时,Java虚拟机将查找异常表,并依此决定需要将控制流转移至哪个异常处理器之中。
异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。
这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。
其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。
target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。
捕获异常涉及了如下三种代码块。
- try 代码块:用来标记需要进行异常监控的代码。也就是说,如果这段代码可能出现异常,处理的时候就把它放到try代码块里面,并对它进行监控。
- catch 代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。不过咱们平时练习的时候会因为方便,直接来一个exception搞定,不会写那么多catch代码块。
- finally 代码块:跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源,咱们在学习字节流、字符流的时候就遇到过。
当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。
当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。
如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。
如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。
在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。
finally 代码块的编译比较复杂。当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。
这里有一个小问题,如果 catch 代码块捕获了异常,并且又触发了另一个异常,那么 finally 捕获并且重抛的异常是哪个呢?
答案是后者。也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。不过Java 7 引入了 Suppressed 异常、try-with-resources,以及多异常捕获。后两者属于语法糖,能够极大地精简我们的代码。这些新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息,并且可以精简资源打开关闭的用法。还支持在同一 catch 代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。
最后还有个有点绕的地方:
有关finally代码块:
1、在程序正常执行的情况下,这段代码会在 try 代码块之后运行。
2、如果 try 代码块触发异常的情况下,且该异常没有被捕获,finally 代码块会直接运行,并且在运行之后重新抛出该异常。
3、如果该异常被 catch 代码块捕获,finally 代码块则在 catch 代码块之后运行。
4、在某些不幸的情况下,catch 代码块也触发了异常,那么 finally 代码块同样会运行,并会抛出 catch 代码块触发的异常。
5、在某些极端不幸的情况下,finally 代码块也触发了异常,那么只好中断 这个当前 finally 代码块 的执行,并往外抛异常。
那么当大家了解了Java 虚拟机的异常处理,应该也能理解为什么咱们看代码报错的时候,需要从下往上看的原因。
以上说了一些有关异常处理的try catch 以及 手动抛异常的相关知识,但是实际开发中为了开发效率、代码可读性、以及后续代码方便维护等等原因,我们一定会遇到全局异常处理的问题,这也是常问的面试题。
runtime exception,也称运行时异常,我们可以不处理。出现运行时异常后,系统会把异常一直往上层抛,一直遇到处理代码。如果没有处理块,到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是Exception的子类,也有一般异常的特点,是可以被Catch块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。
如果不想终止,则必须扑捉所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。在这个场景这样处理可能是一个比较好的应用,但并不代表在所有的场景你都应该如此。如果在其它场景,遇到了一些错误,如果退出程序比较好,这时你就可以不太理会运行时异常,或者是通过对异常的处理显式的控制程序退出。
全局异常处理
1、@ControllerAdvice
配合 @ExceptionHandler
实现全局异常处理
接收Throwable类作为参数,我们知道Throwable是所有异常的父类,所以说,可以自行指定所有异常
比如在方法上加:@ExceptionHandler(IllegalArgumentException.class),则表明此方法处理
IllegalArgumentException 类型的异常,如果参数为空,将默认为方法参数列表中列出的任何异常(方法抛出什么异常都接得住)。
2、@ControllerAdvice
配合 @ModelAttribute
预设全局数据
实际上这个注解的作用就是,允许你往 Model
中注入全局属性(可以供所有Controller中注有@Request Mapping的方法使用),value
和 name
用于指定 属性的 key
,binding
表示是否绑定,默认为 true
。
3、@ControllerAdvice
配合 @InitBinder
实现对请求参数的预处理
@ControllerAdvice
然后,我们来看一下此类的注释:
这个类是为那些声明了(@ExceptionHandler、@InitBinder 或 @ModelAttribute注解修饰的)方法的类而提供的专业化的@Component , 以供多个 Controller类所共享。
说白了,就是aop思想的一种实现,你告诉我需要拦截规则,我帮你把他们拦下来,具体你想做更细致的拦截筛选和拦截之后的处理,你自己通过@ExceptionHandler、@InitBinder 或 @ModelAttribute这三个注解以及被其注解的方法来自定义。
初定义拦截规则:
ControllerAdvice 提供了多种指定Advice规则的定义方式,默认什么都不写,则是Advice所有Controller,当然你也可以通过下列的方式指定规则
比如对于 String[] value() default {} , 写成@ControllerAdvice("org.my.pkg") 或者 @ControllerAdvice(basePackages="org.my.pkg"), 则匹配org.my.pkg包及其子包下的所有Controller,当然也可以用数组的形式指定,如:@ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"}), 也可以通过指定注解来匹配,比如我自定了一个 @CustomAnnotation 注解,我想匹配所有被这个注解修饰的 Controller, 可以这么写:@ControllerAdvice(annotations={CustomAnnotation.class})
还有很多用法,这里就不全部罗列了。
常见的几种RuntimeException如下:
NullPointerException - 空指针引用异常
ClassCastException - 类型强制转换异常。
IllegalArgumentException - 传递非法参数异常。
ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
IndexOutOfBoundsException - 下标越界异常
NegativeArraySizeException - 创建一个大小为负数的数组错误异常
NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
空指针异常的相关知识点
什么时候出现空指针?
null值参与运算(字符串变量未初始化)、
接口类型的对象没有用具体的类初始化、
对象为null的情况下去调用该对象所拥有的方法或者成员变量造成的、
当访问或修改一个对象不存在的字段时会产生异常obj.method()、
去数据库里查询一个空的对象,也是空指针
Assert(断言) 替换 throw exception
:Assert.notNull(user, "用户不存在."); ===》 if (user == null) {
throw new IllegalArgumentException("用户不存在.");
}