Introduction
我们开发的业务系统,或者是产品,常常面临着这样的问题:
- 系统运行出错,但是完全不知道错误发生的位置;
- 我们找到了错误的位置,但是完全不知道是因为什么;
- 系统明明出了错误,但是就是看不到错误堆栈信息。
什么时候需要自定异常?
经常看到一些项目,在全局定义一个 AppException,然后所有地方都只抛出这个异常,并且把捕获的异常case到这个AppException中。
这样做会有如下问题:
- 浪费log日志存储空间,并且栈顶并不是最接近发生异常的代码位置;
- 只有一种异常类,无法精准区分开异常类型;
- 异常类后期难以修改以增加其携带的信息。
什么情况需要手动处理异常?
- 你有能力处理异常,并且你知道如何处理;
- 你有责任处理异常。
如何优雅地处理异常
首先,有一点我们要清楚:MVC设计模式告诉我们,Controller是用来接收页面参数,并且调用逻辑处理,最后组织页面响应的地方。不建议在Controller进行逻辑处理,Controller只应该负责用户API入口和响应的处理。
思考一下如果有一天Service的代码打包成jar放到另一个平台,没有controller了,该怎么办?
定义Service异常
1 | open class BaseExp(val code: Int, error: String) : RuntimeException(error){ |
这个异常继承自RuntimeException,并且包含一个接受一个错误原因的构造器,这样Controller层也不需要知道异常,只要全局捕获到ServiceExp做统一的处理即可。
这无论是在Struct1、2时代,还是SpringMVC中,甚至Servlet年代,都是极为容易的。
此外,异常不能有无参构造函数,因为绝对不允许你抛出一个逻辑处理异常,但是不指明原因。
Controller处理举例
1 | /** |
数据校验一般分为两部分:
- 有效性:比如用户所在岗位,是否属于数据库有记录的岗位ID,如果不存在,无效。
- 合法性:比如用户名只允许输入最多12个字符,用户提交了20个字符,不合法。建议使用校验框架,比如JSR303
假如出现以下问题,如何通知用户?
- 要修改的用户ID不存在;
- 用户被锁定,不允许修改;
- 乐观锁机制发现用户已经被被人修改过等等。
常见的处理方式有:
- 在Controller调用UserService的checkUserExist()方法;
- 在Controller直接书写业务逻辑;
- 在Service响应一个状态码机制,比如1 2 3表示错误信息,0表示没有任何错误。
根据MVC设计模式来说,第一种和第二种都不考虑。而第三种似乎是一种可选的方式,可是如此一来,用户保存逻辑变了,比如增加一个情况,不允许修改已经离职的用户,那么我们还需要修改Controller的代码,代码量增加,维护成本增高,并且还耦合了Service,不符合MVC设计模式。
Service处理举例
1 | /** |
只要我们检查到不允许保存的对象,我们就可以直接throw一个新的异常,异常机制会帮助我们中断代码执行。
Service上层处理异常的方式有:
- 在Controller使用try-catch进行处理;
- 直接把异常抛给上层框架统一处理。
第一种方式直接pass掉,注意我们抛出的ServiceExp,它仅仅逻辑处理异常,并且我们的方法前面也没有声明throws ServiceExp,这表示它是一个非受查异常。
Q:为什么不定义成受查异常? 如果是一个受查异常,那么意味着Controller必须要处理你的异常。并且如果有一天你的业务逻辑变了,可能多一种检查项,就需要增加一个异常。反之需要删除一个异常,那么你的方法签名也需要改变,Controller也随之要改变,这又变成了紧耦合。
Q:可以为每一种检查项定义一个异常吗?可以,但是那样显得太多余了。因为业务逻辑处理失败的时候,根据我们需求,我们只需要通知用户失败的原因(通常应该是一段字符串),以及服务器受理失败的一个状态码,这样这需要一个包含原因属性的异常即可满足我们需求。
ControllerAdvice处理举例
1 | "com.xxx.bussiness.xxx" }) (basePackages = { |
注意一点,在这个类中,我们定义了2个log对象,分别指向ServiceExp.class 和 ControllerAdvice.class,处理ServiceExp的时候使用了info级别的日志输出。
- ServiceExp一定要和其他的代码错误分离,不应该混为一谈。
- ServiceExp并不一定要记录日志,我们应该提供独立的log对象,方便开关。
异常分类建议
- 逻辑异常类:用于描述业务无法按照预期的情况处理下去,属于用户制造的意外。
- 代码错误类:这类异常用于描述开发的代码错误,例如NPE、ILLARG等都属于程序员制造的BUG。
- 专用异常类:多用于特定业务场景,用于描述指定作业出现意外情况无法预先处理。
各类异常必须要有单独的日志记录,或者分级,便于日志管理。比如,有的时候仅仅想给三方运维看到逻辑异常。
Counter-Example
1 | /** |
修改后的代码,如下:
1 | /** |