异常处理最佳实践
一、异常的分类
常规分类:
1、运行时异常(RuntimeException);
2、编译时异常(CheckedException)
用途分类:
1、打断(终止)程序继续往下运行;
2、打断程序继续往下运行,并将异常原因和信息送往上层。
特点分类:
1、可以获得异常的原因;
2、可以获得异常的代号;
3、可以获得异常的错误行号;
4、可以获得异常的堆栈信息(程序运行轨迹);
5、可以获得异常的类型;
……等。
判断分类:
1、可以预判(预先判断)、自主定义的异常,比如我们自己写程序,在Service中,当 (id==null && type==2) 时,抛出一个AbcException异常;
2、不可预判、不透明的异常,比如工具库内部的异常(SQLException、IBEException)等。
针对这些不同类型的异常,他们的使用方式 和 处理方式,是不一样的,详见下面的分析。
二、异常的常见使用场景分析
1、举例1:假设有web、service、dao三层,但是在dao层报错,抛出SQLException或者其他DAO工具库内部的Exception。
对于这种情况,我们通常需要日志记录异常信息;而在web层反馈到view页面上,则不会告诉用户底层的错误原因,只是告诉用户出现了未知异常,或者大概是什么原因出错。
分析:1)工具库内部的错误,比如SQLException,对我们来说,是不可预知的,我们不知道何时、在什么情况下会出现错误,也就是说无法预先判断。2)dao层报错,需要通知上层,也需要记录日志,这个日志是在dao层记录,还是让上层决定 是否记录? 其实这是个普遍问题,放到service层或者通用工具类中,也有这个选择:到底是自己记录日志并且把异常信息往上层抛,还是自己不记录日志,只把完整的异常信息传递给上层,让上层决定是否记录日志。我赞成的是后者,通常情况,我们统一的在中间层都不记录日志信息,有异常直接往外层throw抛出,对开发者而言非常方便(这才是重点)。
2、举例2:接上例,假设我们在service层中,做了一个判断:当 (password不正确) 时,往外抛出一个自定义的异常,异常信息为 “密码错误!!”。对于这种情况,通常情况下,我们不需要记录日志,因为这个错误是我们自己定义的,而且是可以预料的,错误信息的作用是告诉用户,而不是作为日志分析作用。
分析:1)明确一点,对于这种类型的错误,我们不需要记录日志,但是仍然要向上层返回错误信息,并且注意到一点,我们只需要错误信息message,不需要异常的行号、运行时的堆栈信息等。
三、Java异常处理分析
找准异常处理的出口。
四、异常处理最佳实践
1、以 STC票控项目 为例,整个项目的异常,分为三层:
第一层:最基础的父类
BasicCheckedException (继承于Exception)
BasicRuntimeException (继承于RuntimeException)
(注意,整个项目,不要直接new新建 Exception 和 RuntimeException )
第二层:
NestedCheckedException (继承于BasicCheckedException)
NestedRuntimeException (继承于BasicRuntimeException)
第三层: 在上面4个异常类的基础上,扩展的异常类型。
目前有:
StcOrigException (原始异常,用于代替 Exception)-继承于BasicCheckedException
StcOrigRuntimeException (原始异常,用于代替 RuntimeException)-继承于BasicRuntimeException
StcNestedException (嵌套包装异常,用于将原始异常包装起来)-继承于NestedCheckedException
StcNestedRuntimeException (嵌套包装异常,用于将原始异常包装起来)-继承于NestedRuntimeException
StcI18nException (I18N国际化异常,包含errorCode和errorMsg,可对应中文、英文、繁体等异常信息)-继承于BasicCheckedException
StcNoLogException (打断但是不记录日志的异常,仅用于打断程序执行,但是外面不再记录异常信息)-继承于BasicCheckedException
2、最佳实践说明
说得简单点,其实项目中,用得最多的异常为 StcNestedException、StcI18nException、BasicCheckedException,前两者用于new创建异常,后者BasicCheckedException用于声明throws异常。举例如下:
1 public class UserDAO {
2
3 pubic User queryById(Long id) throws BasicCheckedException {
4
5 if(id<0) {
6
7 throw new StcNestedException("ID小于0的用户不存在!");
8
9 }
10
11 Object uobj = null;
12
13 try {
14
15 uobj = getDao.query("select from User");
16
17 } catch (Exception e) {
18
19 throw new StcNestedException(e);
20
21 }
22
23 return UserTools.toUser(uobj);
24
25 }
26
27 }
这样写有几个好处:
1)对于new StcNestedException("ID小于0的用户不存在!"),我们不需要记录异常的堆栈和行号,只需要把错误信息往外抛出即可。此处也可以换成用I18n国际化errorCode代码标识的异常: new StcI18nException(ErrorCode.USR023, id)。
2)对于getDao.query("select from User")可能抛出的SQLException等异常,我们套用了StcNestedException,它可以原封不动的把原始异常的完整信息(比如message、行号和堆栈)保存下来。
四、异常和日志处理的关系
异常处理 和 日志处理 有密切关系,但是不能混为一谈。可以这样说:
1)出现异常 不一定要 记录日志。
2)记录日志 也不一定 是出现异常的时候。
就异常和日志处理的 常见关联点,举例做一个说明:
1、例(一)
Dao层报错,需要通知上层,也需要记录日志,这个日志是在dao层记录,还是让上层(比如Service层)决定 是否记录?
分析:
其实这是个普遍问题,放到service层或者通用工具类中,也有这个选择:到底是自己记录日志并且把异常信息往上层抛,还是自己不记录日志,只把完整的异常信息传递给上层,让上层决定是否记录日志。
我赞成的是后者,通常情况,我们统一的在中间层都不记录日志信息,有异常直接往外层throw抛出,对开发者而言非常方便(这才是重点)。
2、例(二)
因为某种业务需要,我们要在某个Prosessor类中记录日志信息,同时也要终止程序执行。
我们的需求:1)记录错误信息;2)打断程序运行。
传统做法:
1
2
3
4
5
6
|
try { doService(); } catch (AbeException e) { log.error(e); throw e; } |
这种做法能满足上面的需求,但是又超过了我们的需求。因为它 throw e 虽然终止了程序的执行,但是它把错误信息和错误堆栈,都抛给了外层,而外层捕获到这个错误后,又可能会再次记录日志信息,这样就造成了日志信息多出重复,而且往往日志的堆栈信息非常长,看着很吃力。
以上问题就在于,我们只想“打断程序运行”,并不想把异常堆栈信息再继续往外传递。
这个时候,上面提到的“异常处理最佳实践”就提出了一种方案,一种只打断程序执行,不记录堆栈信息的异常——NoLogException。用NoLogException来改造上面的程序,就成了:
1
2
3
4
5
6
|
try { doService(); } catch (AbeException e) { log.error(e); throw new NoLogException(); } |
这样,外层可以判断是否为 NoLogException,如果是则外层不记录日志。退一步讲,即使外层不判断是否为NoLogException,它也可以记录日志信息,但是在NoLogException中没有任何异常的信息(只有一个错误代号),想记录也记录不到。