一. 前言
异常是开发中,非常常见的。异常在不同的场景中,有不同的作用。本文就聊一聊,在 java 应用、线程、虚拟机、通信框架等场景中,对异常常见的处理方式。
二. 语言场景下的异常
首先是超类 java.lang.Throwable ,超类有两种类型的子类,一类是 java.lang.Error ,一类是 java.lang.Exception 。前者是用来描述 java 内部在运行时出现错误或者资源耗尽情况,不应该被 catch 捕获。后者分为两种情况,一种是 java.lang.RuntimeException 和其他实现了父类的异常。
RuntimeException 在被 throw 出去的时候,方法上不需要声明,直接就可以抛出。而其他受检异常需要声明抛出的异常类型。
比如 java.lang.String 的构造方法,就会根据不同的场景抛出不同种类的异常。
在构造 String 解码出现异常,会抛出受检异常 java.lang.IOException 的子类:
public String(byte bytes[], String charsetName) throws UnsupportedEncodingException { }
在使用带有 charset 的构造方法,并且参数为 null 时,不需要在方法上增加签名,直接抛出即可:
if (charset == null) throw new NullPointerException("charset");
一般来说,未受检异常的抛出场景是:错误的类型转换、数组访问越界、访问空指针等。受检异常的抛出场景是:试图在文件尾部后面读取数据、试图打开一个错误格式的 URL、试图根据给定的字符串查找 Class 对象而类不存在。
三. 业务场景下异常的处理
业务场景,一般是声明 BizException 继承 RuntimeException 。不同种类的异常,比如参数校验异常、数据库语句执行异常、第三方依赖异常等,还可以细分,最终由统一的异常处理类进行处理,然后返回特定的状态码。
阿里巴巴编码规范中,有这么几条异常异常处理要求很规范:
1.异常不要用来做流程控制,条件控制。
2.catch 时,分清稳定异常和不稳定异常,不稳定异常最好还进行细分,分别处理。
3.异常捕获了一定要处理,如果不处理一定不要捕获。
4.在调用 RPC、二方包、或者动态生成类等方法时,catch 时捕获 Throwable。
四. 线程对异常的处理
点开 java.lang.Thread#start() 方法,可以看到线程的执行,是调用了本地方法 :
private native void start0();
本地方法的执行过程,与 java 层面无关。虚拟机在执行线程中方法遇到异常并且未被捕获时,会调用 Thread#dispatchUncaughtException() 进行处理。如果线程没有重写这个方法,则虚拟机会一直向上抛出,直到被捕获或者终止线程。
主线程无法捕捉子线程的异常,但是主线程可以使用 FutureTask 作为钩子,在主线程中调用 get() 方法,这样就可以在主线程中捕捉异常。
在虚拟机中,每个含有 catch 语句的字节码中都附带了一个异常表,它包含每个异常 try 语句块的条目(entry)。每个条目都有四个信息,try 包裹的语句块的起点、try 包裹的语句块的终点、跳转的 pc 偏移位置、以及该异常类所在常量池中的索引。
如果在运行时抛出一个异常,虚拟机会按顺序找匹配的条目,如果匹配到,则把 程序计数器 偏转到上述 跳转的 pc 偏移位置上,然后继续执行指令。如果没有匹配到,则弹出当前栈帧,向上继续抛出异常,上级调用方会继续查找异常表。
五. 通信框架对异常的处理
Throwable 实现了 Serializable 接口,表明异常也是可以被序列化的。
RPC 框架的处理流程,就是把 java 类序列化后通过网络传输给调用方,调用方进行反序列化。
一般的 RPC 框架,比如 dubbo 和 feign ,都有一个统一的异常处理类。
dubbo 就是 com.alibaba.dubbo.rpc.filter.ExceptionFilter,这个类中对不同的异常有不同的处理方式,受检异常直接抛出,其他异常,原则上如果调用方是可以进行反序列化为 java对象 的就直接抛出,如果不能,则使用 RuntimeException 包裹后再抛出。
feign 默认是通过 http 协议进行网络传输,序列化方式是 json字符串。所以如果被调用方抛异常了,返回给调用方的,就不是异常对象,而是异常信息。springboot web 项目,异常时返回的信息就是 status、timestamp、message、error、path,仅通过这些信息,是没法反序列化成 java 对象的。所以 feign 的异常处理,是获取 message 信息,然后包装成 FeignException 返回出去。