对Java Exception的思考
背景
在对接项目设计接口时,对异常设计通常疑问:
- 为什么抛出的异常都是
Runtime
的异常? 什么时候应该用Checked Exception
? - 读接口要不要抛出业务异常?
- 异常何时捕获最佳?
要回答这一系列问题,并不是一个容易的事情,且听我娓娓道来。
两种异常
我们通常在概念上将异常分为Checked Exception
和 Runtime Exception
这两种异常,那么接下来我们来看看两种异常的层级图。在网上有很多的层级图是这样子的:
但真正在代码的时候却是这样的:
在代码中并没有一个CheckedException的class,或者说Exception就是CheckedException。
明白了异常的层级结构,那么我们来看看两种异常使用上的区别。
Checked Exception
必须明确捕获进行处理,如果不捕获,那么就会编译的时候报错。
RuntimeException
则不然,这个异常是无需明确捕获的,是在运行时进行检查的。
那么接下来我们看一下为什么都这么分?或者这么分的好处是什么?
这里要引经据典了,我比较赞同Effective Java书中的一个建议方式:
Item 58: Use checked exception for recoverable conditions and runtime exceptions for programming errors
使用CheckedException来表示可以恢复的异常,使用RuntimeException来表示编程出的问题。
但是回到真实的情况里,却发现情况往往不是这样的。我们的微服务接口通常都是抛出的RuntimeException,RPC解耦层也不抛出CheckedException,这是为什么呢? 这样子是对的吗?
回到现实
面对上面的问题,我不禁陷入了沉思。道理归道理,现实归现实的吗?
在分布式的经典理论中,微服务都被认为是不可靠的,所以我们不更应该去捕获去做处理,去重试吗? 等等,重试?
想到重试的时候,感觉抓到了什么,在微服务的调用框架中,往往都会提供重试的能力,也就是说虽然没有用单个微服务没有声明CheckedException,但是框架总是认为它会出错的,所以进行了捕获并且进行了重试,所以也是间接起到了checked exception的作用,符合理论上讲的”可恢复的资源建议捕获进行重试“。而且微服务的调用也比较特殊,通常都是通过反射调用,所以微服务接口抛不抛CheckedException本身并不重要。不过对于微服务来讲,大部分的场景是需要立即返回的,做资源重试直到恢复往往是不现实的,基本上超过了几秒钟后,用户已经不再当前页面进行等待了,所以在配置重试的时候要很谨慎才可以。
接下来看RPC的接口。我们的RPC接口通常都不会抛CheckedException,这说到底本质上还是重试的必要性和价值问题,和上面微服务考虑的是一样的,而且框架都已经做了重试,没有必要做相同的处理了。使用RuntimeException的好处是是流程变得简单,统一在出口处进行捕获处理就可以。
总结下来,对于资源类的服务,需要捕获异常且重试,建议是使用CheckedException明确告诉调用方进行捕获。
总结完了,让我们来回答一下开篇的问题。
解决问题
- 为什么抛出的异常都是
Runtime
的异常? 什么时候应该用Checked Exception
?
资源类的建议都抛CheckedException,如果在框架有重试的情况下,可以选择不抛,由框架进行处理。 - 读接口要不要抛出业务异常?
建议都走Result,不仅明确,而且能够减少序列化的成本。Java语言的异常堆栈构建一次还是比较耗费资源的。 - 异常什么时候捕获?
异常有两个作用: 提供阻断信号 和 阻断信息。我们如果可以通过处理阻断进行自动处理,就可以在需要的时候进行捕获;如果需要阻断的信息,依赖来说要遵守两个原则: 边界隔离和延迟捕获的原则。边界隔离是指将工作内容隔离的地方进行捕获并转化,成为内部统一可以理解的错误;延迟捕获是指在真正需要的地方进行处理,而不必每一层都进行处理。
参考资料:
- Effective Java
- 如何优雅的处理异常