异常处理规范
1.目 标
本文旨在介绍NCV5环境下,采用中间件抽象框架提供的开发模型,对异常定义、使用和捕捉等的处理规范。
2.异常的分类
从异常(错误)的紧要程度和通常反应的处理逻辑来讲,程序员所要关心的异常主要有四类:
ØJVM错误: 这种类型的错误由 JVM 抛出。OutOfMemoryError
就是 JVM 异常的一个常见示例。对 JVM异常您无能为力。它们表明一种致命的情况。唯一得体的退出办法是停止应用程序服务器(可能要增加硬件资源),然后重新启动系统。
Ø非JVM错误(Error): 表示对整个系统的处理具有严重影响的错误,如cache溢出,中间件框架启动失败等等。这类异常一般需要中间件方面的参数调整或者重新启动系统。
Ø非受查异常:在大多数情况下非受查异常由JVM 作为 RuntimeException
的子类抛出。例如,NullPointerException
或 ArrayOutOfBoundsException
将因代码中的错误而被抛出。另一种类型的非受查异常在系统碰到配置不当的资源时发生,在这种情况下,系统不能进行合适处理,因此作为非受查异常抛出颇有意义。还有的规则是,如果您对某个异常无能为力,那么它应当重新封装为非受查异常抛出。
Ø受查异常:受查异常是一种定制异常,由应用程序或第三方的库抛出,它们预示了处理逻辑中的某个条件尚未满足。
从中间件抽象框架和程序员使用的角度来看,程序员主要关心两类异常,一类为系统异常,一类为应用程序异常,这两类异常在中间件抽象框架和业务方法中都可能遇到,并且处理,因此我们对他们进行更加详细地说明:
Ø中间件框架非受查异常:这类异常来自于FrameworkRuntimeException,对于这类异常程序员通常不需要特别的逻辑处理,需要通过配置中间件和框架进行解决
Ø业务模块的非受查异常:业务处理模块如果对于一些处理上的逻辑没有处理能力,需要抛出业务模块的系统异常,该类异常来自BusinessRuntinmeException
Ø中间件抽象框架受查异常:这类异常来自于FrameworkException,业务模块通常需要捕作他们处理,或者重新封装为业务模块的非受查异常,或者进行适当的处理,然后封装作为业务模块的受查异常抛出,或者完全处理恢复。目前中间件这类异常主要是ComponentExcetion
Ø业务模块的受查异常:这类异常来自于BusinessException,由各个业务模块抛出和处理。
为了简单起见,我们把中间件框架的非受查异常和业务模块的非受查异常统一称为系统异常,而中间件抽象框架的受查异常和业务模块的受查异常统一称为为应用异常,他们在中间件框架的异常处理模型中代表了两类不同的处理过程。
3.框架异常处理模型
NC的系统基于B/S结构,从Browser端发起的调用需要经过中间件抽象框架的接入框架进行远程接入,另一方面从效率考虑,服务端对服务端的调用请求没有经过远程的接入过程,而是直接按照引用的方式发起了调用。无论哪种调用方式,中间件抽象框架都需达成如下的目标:
Ø中间件抽象框架对应用异常不进行任何包装,直接把异常抛给客户端(Browser或者本地客户程序)
Ø远程BService(带有事务)的业务方法抛出异常会导致当前事务回滚
Ø中间件系统异常(如TransactionRollbackException等)在抛出给Browser进行了会转化为BusinessRuntimeException抛出,转化之前中间件抽象框架对异常进行了日志
Ø中间件抽象框架的非受查异常抛出前都进行了日志
4.定义异常
程序员只能定义BusinessException和BusinessRuntimeException的子类,每个产品模块都有改模块内公共的基类异常,他们来自于BusinessException,和BusinessRuntimeException,异常的定义必须严格的按照这个异常的层次进行。
如系统管理框架:
package nc.vo.sm.exception; importnc.vo.pub.BusinessException; public class SystemManagerException extendsBusinessException { … } |
该产品模块的异常都需要从这异常继承而来,如无效的用户异常:
package nc.vo.sm.exception.sm; public class InvalidUserException extendsSystemManagerException { … } |
总结起来异常定义的规则如下:
Ø产品模块定义两个基本异常(受查的和非受查的),该异常来自BusinessException或者BusinessRuntimException
Ø该产品模块的所有的子异常来自这两个异常
Ø异常包以模块>作为基本的包头
Ø所有的异常名称必须以Exception结尾
Ø所有的一异常定义都必须包含异常链,提供对原始异常的跟踪:
package nc.vo.sm; public class InvalidUserException extendsSystemManagerException { public InvalidUserException(String msg,Throwable throwable) { super(msg, throwable); } public InvalidUserException(Throwablethrowable) { super(throwable); } … } |
Ø
5.抛出异常
中间件抽象框架对异常进行了某些特别的处理,特别是针对远程的BService,因此异常抛出需要结合业务逻辑进行,总结规则如下:
Ø业务模块方法throws部分的受查异常都需要来自业务异常(BusinessException)和BusinessRuntimeException
Ø业务代码不能直接抛出没有任何业务意义的异常如throw new Exception(“msg”)
Ø对于远程BService,如果你需要在BService方法中,不希望潜在的异常导致事务回滚,那么请不要在该Bserivce的对应方法throws语句中指定任何的异常,而且不要在实现中抛出任何非受查一场
例如定义BService接口时用下面的形式:
public void service();
而不是
public void service() throws BusinessException
Ø如果受查异常由于调用中间件框架导致(FramworkException子类),但问题在于客户请求本身(如查找的名称不存在),首先进行业务日志,并对他封装为业务异常然后抛出,并且不要保持异常链。
Ø如果受查异常由于调用中间件框架(FramworkException子类)导致,但是不在程序员意料之内的异常,首先进行日志,然后封装为BusinessRuntimeException的子类异常,并且不要保持异常链。
Ø抛出的异常必须具备一定的描述该异常发生的原因的信息,以便定位错误
Ø除了上述描述中要求不要保持异常链的情况外,其他的异常重新抛出后最好保持异常链。
throws语句如何写如下面例子
package nc.intf.sm; public interface SMBService { public boolean validUser(String user);//不抛出任何异常,因此事务永远不会自动回滚 public void validate(String user) throwsInvalidUserExceptoion; //来自BusinessException … |
捕作到中间件的异常,抛出处理如下:
package nc.bs.sm; public interface SMBServiceImpl { public boolean validUser(String user){ try{ …} catch(ComponentException exp) { //能够进行良好的逻辑处理 … } } public void validate(String user) throwsInvalidUserExceptoion { try{ …} catch(ComponentNotFoundException exp){ //可以正常处理 throw new InvalidUserException(“caused by:”, exp); }catch(FrameworkRuntimeException aa) { //无法正常处理 throw new InvalidUserRuntimeException(“Causedby: “ ,aa); } … |
6.捕捉异常
异常捕作需要考虑两个因素,一个是异常捕捉的粒度,一个敏感资源的释放,异常捕捉的粒度需要考虑该捕获什么样的异常,不该捕获什么样的异常等问题。异常捕捉的使用规则主要描述如下:
Ø捕捉异常应该尽量的精确(代码逻辑范围内),精确包括异常的类和异常的出向区域
Ø控制try/catch的规模,通常代码中由多个位置抛出多种异常,针对不同的异常有不同的处理时try/catch可以细致一点,对每一类异常单独的进行捕捉。
Ø异常捕捉时,最一般的异常永远放在最后的catch部分
Ø如果代码中涉及到敏感的资源如Connection,Socket,文件等,一定要加上finally处理块,释放资源
Ø不要吃掉可能导致业务问题的异常
ØNullPointException,ArrayIndexOutofBOundException,SQLExceptin, DbException、IllegalArgumentException等异常,以及中间件框架中抛出的ComponentException通常是业务相关的,需要按照业务逻辑进行捕捉处理
Ø对于远程的BService,如果需要捕作非受查的异常,请捕作BusinessRuntimeException,非受查异常在中间件框架处理后,到达客户端的异常不一定是在原始代码中跑出的非受查异常,因此需要特别注意。
Ø如果异常需要抛出给前端应用(ui),出于安全的考虑可能不希望前端程序保留异常的链,这时候需要首先日志改异常,然后把该异常转化为业务异常,去掉异常链抛出
Ø如果异常已经在throws语句中抛出,一般不需要进行捕捉和处理
7. 处理异常
异常捕捉后的处理为异常处理的一个关键部分,异常处理一方面需要恢复业务逻辑的得一致性,另外一方面还需要为系统提供良好的跟踪能力。因此异常处理我们可以按照下面的原则进行:
Ø如果捕捉到异常后,不会再次把异常抛出(吃点异常),如果有跟踪的必要性对异常进行日志工作,日志的级别根据业务逻辑可以选用debug或者warn。对于对系统的表现有一定影响的采用warn,而几乎没有影响的采用debug
Ø如果捕捉到的异常需要再次包装抛出,包装后的异常必须来自BusinessException或者BusinessRuntimeException
Ø如果捕捉了InvokeTragetException,不能把该异常直接抛出,而是需要对其中的目标异常进行处理(getTargetException())
Ø中间件抛出的SystemException,NamingException异常应用程序捕捉后首先进行日志,然后转化为BusinessException/BusinessRuntimeException的子类,不需要保持异常链
ØSQLException必须进行捕捉和处理,进行日志处理,并且转化为对应的BusinessException/BusinessRuntimeException的子类,不需要保持异常链
Ø如果捕捉到的异常,处理抛出后,去掉了异常链,对原始异常应该首先进行日志
Ø如果异常抛出后,保持了异常链,在抛出之前对原始的异常不需要进行日志工作
下面作为一个例子我们描述一下异常处理的基本过程
packagenc.bs.demo.finance; importnc.bs.demo.hr.HRException; importnc.bs.demo.hr.IHRService; importnc.bs.demo.supply.ISupplyService; importnc.bs.demo.supply.SupplyException; importnc.bs.framework.common.NCLocator; importnc.bs.framework.exception.ComponentException; importnc.bs.logging.Log; importnc.vo.pub.BusinessRuntimeException; public classSampleBServiceImpl { //根据需要选择适合的日志API,如果动态日志采用Logger private static final Log log =Log.getInstance(SampleBServiceImpl.class); public void service(String account) throws FinanceException{ try { ISupplyService supplyService = (ISupplyService)NCLocator.getInstance().lookup(ISupplyService.class.getName()); supplyService.service(account); IFinanceService finService = (IFinanceService)NCLocator.getInstance().lookup(IFinanceService.class.getName()); finService.balance(account); IHRService hrService = (IHRService)NCLocator.getInstance().lookup(IHRService.class.getName()); hrService.service(); } catch (ComponentException e) { //该异常作为业务处理经常要面对的异常因此我们做一些包装,但是我们不希望抛出 //去的异常暴露我们的非业务的异常链,因此首先日志它,然后重新包装出去 log.error("处理账户:"+ account + " 出现错误",e); throw new FinanceException("所要的服务没有安装,请购买该服务模块: " + IFinanceService.class); //e.printStackTrace(); } catch (FinanceException e) { //异常为本模块业务的异常,因此抛出,一般情况下不用捕捉该异常 throw e; }catch (SupplyException e) { //其他模块的业务异常,包装抛出,保持异常链 throw newFinanceException("服务失败",e); } catch(NullPointerException e) { //特别的异常处理,预料之外的异常,包装抛出,保持异常链,也可以根据需要抛出为BusinessRuntimeExcepiton的模块子类异常 throw newFinanceException("服务失败",e); //throw new FinanceRuntimeException("服务失败",e); } catch (HRException e) { //该异常对业务没有任何影响,因此我们吃掉该异常,但是为了跟踪,我们需要日志 log.debug("通知人力资源失败:" + e.getMessage()); //如果改行为比较要紧采用警告 //log.warn("通知人力资源失败", e); } catch(BusinessRuntimeException e) { //最普通的异常放在最后面处理,这里我们需要对BusinessRunitmeException进行处理 //log.error("服务失败",e); //throw new FinanceException("服务失败",e); } } } |
8.方法覆盖
在类继承的时候,经常需要覆盖父类的方法,这时候应当按照约定的规则进行异常的处理,例如:
package nc.bs.framework.connector.client; public class NCCLassLoader extends ClassLoader { public voidloadClass(String name) throws ClassNotFoundException { Class clazz = null; try { //基于网络的的列加载处理 } catch(IOException ioe) { } return clazz; } } |
这样的处理是不符合ClassLoader的一般约定的,因为系统在没有加载到类的时候,根据协议应该抛出ClassNotFoundException,而这里返回为null。
一般的类似的处理出现在具有返回值和具有异常抛出的方法中,对于这样的方法我们要十分仔细的约定什么时候抛出异常,什么时候正常返回,避免不恰当的处理。
9.服务端/客户端异常
在客户服务器的体系下,客户端的逻辑需要服务端进行处理,这时候一些特定服务端的异常可能会被抛出给调用方。
NC的系统可以跨平台的运行在多种应用服务器之下,客户端的代码不应该看到特定服务端的异常,因此对这类异常必须经过处理才能抛给客户端。如WEBSPHERE特定的异常。
对于服务端/客户端异常处理的要求在框架中,要求接入框架进行特定的异常处理,对于这些特别的服务端异常,系统以业务框架是别的BusinessRuntimeException的形式进行抛出。
应用程序抛出的RuntimeException,在捕作的时候只能期望捕捉到BusinessRuntimeException,而不能为原始的RuntimeException,原因是中间件的容器对异常进行了特别的处理。
10. 例子分析
针对规范描述,我们分析下面的例子:
1OutputStreamWriterout=...
2java.sql.Connectionconn=...
3try{//⑸
4Statementstat=conn.createStatement();
5ResultSetrs=stat.executeQuery(
6“selectuid,namefromuser”);
7while(rs.next())
8{
9out.println(“ID:”+rs.getString(“uid”)//⑹
10“,姓名:”+rs.getString(“name”));
11}
12conn.close();//⑶
13out.close();
14}
15catch(Exceptionex)//⑵
16{
17ex.printStackTrace();//⑴,⑷
18}
问题:
1.丢弃异常:代码:15行-18行。从代码看丢弃异常对业务存在影响
2.异常捕捉太泛:代码:15行。直接捕作了Exception
3.占用资源不释放:代码:3行-14行。
4.过于庞大的try块:代码:3行-14行,异常捕作的位置太出
5.没有进行日志
6.由于1,2,3,4的问题导致了逻辑不正确,如数据输出不完整
11.补充
本部分主要补充ComponentException的处理和数据库异常DbException以及SQL异常的处理。
ComponentException异常为一个受查的异常,业务系统在捕作到ComponentException后,如果不能正常处理,把它转化为对应产品模块的业务一场进行抛出,不需要保持异常链,注意的是在抛出新异常之前需要进行日志工作。
DbException和SQLException都是受查异常,业务系统在捕作后,如果不能按照逻辑正常地进行处理,直接转化为产品模块的业务异常,进行抛出,不需要保持异常链,注意在抛出异常之前需要进行日志工作。
业务系统的所有异常都来源于BusinessException,和BusinessRuntimeException,程序员一般不需要关心BusinessRuntimeException。前台对后台的一个请求,如果后台发生的异常为BusinessRuntimeException,中间件需要首先进行日志。除了前台界面对异常给与用户提示外,业务系统一般不需要处理BusinessRuntimeException。
如果在异常处理上,不知道是否要抛出什么样的异常,那么最好按照接口的要求抛出BusinessException体系下的异常,而不是运行时刻的异常。