异常处理最佳实践
2015年06月24日


一、异常的分类

常规分类:

  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)通常,异常的出口为:
web层(action、controller),最终其实归结到 Servlet 和 Filter。

所以,在我们编写程序的最外层,比如action中,是不应该再出现“未捕获的异常”的,因为这样我们就没办法控制了。
因此,我们要在最外层,做好应有的异常处理。

特别提醒,不只是action、controller,一些对外提供服务的Servlet或者Webservice,都算是异常的最终出口。

2)自定义的异常出口
除了web层外,其他地方也可以成为“自定义”的异常出口。
比如,一个utils抛出了异常,我在service里面就捕获了,然后“就地解决”掉,不再继续传播这个异常。
这种情况,就是所谓的“自定义的异常出口”。

异常的处理方式
根据异常的出口分类,有“web层出口的异常”和“自定义出口的异常”。

1、web层出口的异常
又可以分类如下:
1)需要实现异常消息的“国际化”(多语言版本)。
2)不需要“国际化”。

a)异常出口直接面向的是用户或者浏览器。
b)异常出口面向的是其他系统。

    对于1),经典的解决方案就是:定义所有可以预知的错误信息(用errorCode和errorMsg),errorCode对应多个语言版本的errorMsg。
    对于2),则可以不用errorCode和errorMsg,直接往外抛出错误信息即可。但是不推荐这么做。原因如下:
    - 异常信息可能很简陋,如果用堆栈信息,那么堆栈信息又不方便在客户端展示。
    - 异常信息也可能很大,例如一个sql错误,仅errorMessage就可能几千个字符,不便于web端展示。
    - 原生的异常信息,通常是给开发人员看的,对于用户来讲,可能不好理解。

    对于a),可以直接返回异常的具体信息,如果要支持国际化,也是可行的。
    对于b),把异常信息直接返回给其他系统,不是一种好的做法,因为有可能其他系统需要,对异常类型做判断或加工。
    故,最好是,返回errorCode和errorMsg,其他系统可以根据errorCode去做判断或加工。

总结:
    综合考虑 1)2)a)b),一种较好的异常处理方式是: 定义所以可以预知的错误信息,用errorCode和errorMsg表示。必要是可以增加errorMsg的语言版本。
    这就要求我们,在编写异常处理的代码时,不要用中文。要么把错误信息定义成errorCode,要么用简洁的英文描述错误信息。    
    例如,
    try{
           .........
    }catch(Excpetion e){
            throw new MyException(ErrorCode.C0001, e.getMessage());
    }
    或者
    try{
           ........
    }catch(Excpetion e){
            throw new MyRuntimeException(" encode failure! n must big than 0.");
    }
   或者
    try{
           ........
    }catch(IOExcpetion e){
            throw new ExceptionWrapper(e); //用ExceptionWrapper这个工具类,包装原始的错误信息
    }
    这三种写法,都是符合上面的规范的。但是使用场景不同。
    第一种,面向的是较高层次的代码,比如service层、dao层,它可以直接传递到web层去。所以就地定义了ErrorCode。
    第二种和第三种写法,面向的是较底层的代码,比如最底层的工具类。

    在面向客户的项目中,建议只用第一种写法。第二、三种写法,更多的是用于那些面向服务的项目中。

2、自定义出口的异常
   有时候,我们不需要把异常再通知外部,在自己内部处理就行了。
    比如:
    try{
            InputStream in = IOUtils.getInputStream(filePath);
    }catch(IOException e){
            log.warn(e);
    }
    出错时,直接记录日志就OK了,不需要再把这个错误往外抛出。
    
    有时候,也可以这么做:

public void doParse(){
    .......
    try{
           ........
    }catch(IOExcpetion e){
            throw new ExceptionWrapper(e); //用ExceptionWrapper这个工具类,包装原始的错误信息
    }
    ......
}

public void doServiceAA(){
    try{
           doParse();
    }catch(Excpetion e){
            throw new MyException(ErrorCode.C0001, e.getMessage());
    }
}
public void doServiceAA(){
    try{
           doParse();
    }catch(Excpetion e){
           // ignore this error
    }
}
    就是说,错误时,在这个地方不做处理,把错误往外抛出,交给上层去决定怎么来处理这个错误。
    但这种用法还是比较少的。不建议滥用。


四、异常处理最佳实践

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异常。举例如下:

public class UserDAO {
    pubic User queryById(Long id) throws BasicCheckedException {
        if(id<0) {
            throw new StcNestedException("ID小于0的用户不存在!");
        }
        Object uobj = null;
        try {
            uobj = getDao.query("select from User");
        } catch (Exception e) {
            throw new StcNestedException(e);
        }
        return UserTools.toUser(uobj);
    }
}


这样写有几个好处:

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)打断程序运行。

传统做法:

try { 
    doService(); 
} catch(AbeException e) {
    log.error(e);
    throw e;
}

这种做法能满足上面的需求,但是又超过了我们的需求。因为它 throw e 虽然终止了程序的执行,但是它把错误信息和错误堆栈,都抛给了外层,而外层捕获到这个错误后,又可能会再次记录日志信息,这样就造成了日志信息多出重复,而且往往日志的堆栈信息非常长,看着很吃力。

    以上问题就在于,我们只想“打断程序运行”,并不想把异常堆栈信息再继续往外传递

    这个时候,上面提到的“异常处理最佳实践”就提出了一种方案,一种只打断程序执行,不记录堆栈信息的异常——NoLogException。用NoLogException来改造上面的程序,就成了:

try { 
    doService(); 
} catch(AbeException e) {
    log.error(e);
    throw new NoLogException();
}

这样,外层可以判断是否为 NoLogException,如果是则外层不记录日志。退一步讲,即使外层不判断是否为NoLogException,它也可以记录日志信息,但是在NoLogException中没有任何异常的信息(只有一个错误代号),想记录也记录不到。