一、异常的分类
常规分类:
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异常。举例如下:
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 |
|
这种做法能满足上面的需求,但是又超过了我们的需求。因为它 throw e 虽然终止了程序的执行,但是它把错误信息和错误堆栈,都抛给了外层,而外层捕获到这个错误后,又可能会再次记录日志信息,这样就造成了日志信息多出重复,而且往往日志的堆栈信息非常长,看着很吃力。
以上问题就在于,我们只想“打断程序运行”,并不想把异常堆栈信息再继续往外传递。
这个时候,上面提到的“异常处理最佳实践”就提出了一种方案,一种只打断程序执行,不记录堆栈信息的异常——NoLogException。用NoLogException来改造上面的程序,就成了:
1 2 3 4 5 6 |
|
这样,外层可以判断是否为 NoLogException,如果是则外层不记录日志。退一步讲,即使外层不判断是否为NoLogException,它也可以记录日志信息,但是在NoLogException中没有任何异常的信息(只有一个错误代号),想记录也记录不到。