异常处理的那些事儿

420 阅读6分钟

你好,我是梁松华。今天想和你聊的话题是异常处理那些事儿。
异常处理是很多新手搞不懂的逻辑,别人的代码有时进行了异常捕获,有时又不进行捕获,到底是为啥?有什么科学依据嘛?这种代码确定容易让人迷惑,所以今天我就来捋一捋异常处理,帮你一次性搞懂它。
开篇先和你交个底,异常处理确实复杂并且颇有争议。所以我先从系统分类角度来剖析一下异常处理,让你了解一下异常处理的一般套路。
假如你的系统是消费MQ消息的。对于异常是不能轻易捕获的,否则框架无法帮助你进行自动重试。除非业务是非幂等性的,只能处理一次消息。
假如你的系统是服务间调用类型的,给其他系统提供远程RPC调用服务的。当你的业务不涉及到金钱,不涉及到个别类型错误必须由调用者针对性处理的话,也就是说你的调用者不关注各种异常码的差别,一律当成调用失败来处理,那异常信息就直接透传给调用者好了。否则你就需要捕获异常,然后包装错误码返回给调用者,这些错误码通常有非常特殊的含义,调用者必须要处理的。比如明确告知活动池中红包快发送完了,需要及时补充。
服务间调用中还有一种很特殊的类型,就是提供服务给其他系统定时轮询调用。那你的系统就必须捕获异常,借助异常码,显示告知调用者是否需要继续重试,还是直接判定为业务失败。因为有些异常如果一直无脑重试的话,自己服务会抗不住的,没有限流措施的话,恢复成可用状态将是一个噩梦。
再比如你的系统是提供给APP端使用的。那么你必须捕获异常,并且指定很多种错误异常码,方便前端对不同情况进行兜底,因为APP交互比较复杂,有时需要引导用户重新登录,有时又需要引导用户进行其他操作。
最后一种分类,就是你的系统是提供给PC端使用的。那么你必须捕获异常,并且封装错误异常码,因为错误堆栈不应该暴露给用户。但是和APP端不同的是,PC端交互相对简单,所以我觉得异常码和HTTP错误响应码相似即可,能区别服务器异常、请求超时、方法不存在等等情况就可以了,具体错误信息直接展示,无需过度细分。
说完了系统维度的,再来说说每个模块或者每个分层维度的方法。或许你看到过这样的代码,每个模块下的每个方法都使用了try-catch捕获异常,这样真的对吗?或者说,异常处理的边界在哪里?
我们知道,每个方法都是一个含有输入输出的独立模块,只需要完成自己的功能,不需要关注整个流程。同样的道理,每个方法只需要关注内部可以处理的异常,如果碰到处理不了的异常的时候,只需要将特定的异常信息抛出,不需要关心异常是如何处理。
比如,查询详情的方法,详情不存在返回空,和查询异常返回空,是截然不同的两个逻辑,但是作为最原子的方法,它是不知道上层是如何处理查询异常的,所以在这个方法签名中可以指定异常。
类似的还有,新增内容的方法,执行成功时需要返回内容ID,执行失败时,需要将失败原因暴露出去,所以在这个方法签名中也可以指定异常。
相反的例子是,在原子类中处理数据库的持久化,我们可以使用try-catch进行异常捕获,当更新失败时,直接返回错误标识,而不需要通过抛异常的方式,告知执行失败了。因为调用者通常只关心结果是否成功,并且数据库的错误信息可读性非常考验人的功力,完全没必要暴露出去。
其实啊,能用变量和判断语句进行逻辑处理的,还是尽量减少异常的使用。比如,我在包装依赖接口的原子调用方法时,喜欢使用Java1.8提供的Optional类,它是一个包括可选值的包装类,这意味着Optional类可以包含错误信息,还可以为空也就是表示执行正常。这样就不需要显式地使用异常,或者返回特定的数值比如-1,来表示流程异常。这样业务流程中就不会出现过多的异常处理分支,影响代码阅读性,甚至干扰问题排查。
从上面可以看到,我们有时要把异常抛给上层调用者,由上层调用者决定如何进行处理。甚至会把异常一路透传到路由层,也就是我们常说的控制器层,由它来决定如何处理,可以根据异常组装错误码和返回结果,也可以将异常包装后继续透传给接口调用方。
对我来说,透传异常或者使用错误码,本质没有太多区别,但是会影响到监控统一拦截的处理逻辑,因为监控一般都会过滤掉参数校验等非业务异常。但是,如果你的团队使用的是微服务架构,并且将数据持久层和业务通用层分离,导致链路很长的话,那异常信息就不要藏起来了,否则问题排查真的会把人逼疯的。
万一真的必须捕获异常,那异常时的返回值应该是什么呢?
这个问题的答案算得上是编码规范了,也就是当方法签名的返回类型为普通对象时,返回空。当方法签名的返回类型是集合类型时,那就返回空集合。
好,到这里,本文就结束了。希望本次分享能帮助到你,也欢迎你把文章分享给你的朋友,下期见!

关注微信公众号:松华说,获取更多精彩!

BLOG地址:www.liangsonghua.com

公众号介绍:分享在京东工作的技术感悟,还有JAVA技术和业内最佳实践,大部分都是务实的、能看懂的、可复现的