本文主要针对Java异常选择和使用中的一些误区。希望读者能够掌握异常处理的一些要点和原则,注意总结和归纳。只有处理异常,才能提高开发人员的基本素质,提高系统的健壮性,改善用户体验,增加产品的价值。误区一、异常选择图一、异常分类图一描述了异常的结构。其实我们都知道异常分为检测异常和非检测异常,但是在实践中对这两类异常的应用却很混淆。由于非检查异常的易用性,许多开发人员认为检查异常是无用的。其实异常的应用场景可以归纳为:1.调用代码无法继续执行,需要立即终止。这种情况的可能性太多了,比如无法连接服务器,参数不正确等等。这些时间适用于非检测异常,不需要显式捕获和处理调用代码,代码简洁明了。其次,调用代码需要进一步处理和恢复。如果将SQLException定义为非检测异常,开发人员会想当然地认为SQLException在操作数据时不需要调用代码显式捕获和处理,这将导致严重的Connection未关闭,Transaction未滚动back、DB中的脏数据等。在这种情况下,正是因为SQLException被定义为检测异常,促使开发人员在代码产生异常后显式捕获并清理资源。当然,在清理完资源后,可以继续抛出非检测异常,阻止程序的执行。根据观察和了解,检测异常大多可以应用于工具类。误区二、直接在页面或客户端显示异常。直接在客户端打印异常的例子并不少见。以JSP为例,一旦代码运行异常,容器默认会直接在页面打印异常堆栈信息。事实上,从客户的角度来看,任何异常都没有实际意义,绝大多数客户根本无法理解异常信息。软件开发应该尽量避免直接向用户呈现异常。List1packagecom.ibm.dw.sample.exception;/***自定义RuntimeException*添加错误码属性*/publicclassRuntimeExceptionextendsjava.lang.RuntimeException{//默认错误码publicstaticfinalIntegerGENERIC=1000000;//错误码privateIntegererrorCode;publicRuntimeException(IntegererrorCode,Throwablecause){this(errorCode,null,cause);}publicRuntimeException(Stringmessage,Throwablecause){//使用一般错误码this(GENERIC,message,cause);}publicRuntimeException(IntegererrorCode,Stringmessage,Throwablecause){super(message,cause);this.errorCode=errorCode;}publicIntegergetErrorCode(){returnerrorCode;}}如示例代码所示,在异常中引入错误码。一旦出现异常,我们只需要将异常的错误码呈现给用户,或者将错误码转化为更容易理解的提示即可。其实这里的错误码还包含另外一个功能,开发者也可以根据错误码准确知道发生了什么类型的异常。误区三、代码层级污染我们经常把代码分成Service、BusinessLogic、DAO等不同的层级。DAO层会包含抛出异常的方法,如清单2所示:清单2publicCustomerretrieveCustomerById(Longid)throwSQLException{//根据ID查询数据库}上面的代码乍一看没啥问题,但是从角度仔细想想的设计耦合。这里的SQLException污染了上层的调用代码,调用层需要显式的使用try-catch来捕获,或者向上层进一步抛出。根据设计隔离的原则,我们可以适当修改为:List3publicCustomerretrieveCustomerById(Longid){try{//根据ID查询数据库}catch(SQLExceptione){//使用非检测异常封装检测异常,降低耦合度thrownewRuntimeException(SQLErrorCode,e);}finally{//关闭连接,清理资源}}误区4.忽略异常下面的异常处理只是把异常输出到控制台,没有任何意义。而且这里发生了异常,并没有打断程序,然后调用代码继续执行,引发了更多的异常。清单4publicvoidretrieveObjectById(Longid){try{//..somecodethatthrowsSQLException}catch(SQLExceptionex){/***懂的人都知道,这里打印异常是没有意义的,只是把错误栈输出到控制台。*在Production环境下,需要将errorstack输出到log中。*而且这里catch处理后程序继续执行,会导致进一步的问题*/ex.printStacktrace();}}可以重构:清单5publicvoidretrieveObjectById(Longid){try{//..somecodethatthrowsSQLException}catch(SQLExceptionex){thrownewRuntimeException("ExceptioninretieveObjectById",ex);}finally{//cleanupresultset,statement,connectionetc}}这个误解比较基础,一般不会犯这种低级错误?。误区5.将异常包含在循环语句块中如下代码所示,将异常包含在for循环语句块中。清单6for(inti=0;i<100;i++){try{}catch(XXXExceptione){//...}}我们都知道异常处理会占用系统资源。乍一看,所有人都认为不会犯这样的错误。从另一个角度看,A类中执行了一个循环,循环中调用了B类的方法,但是B类中被调用的方法中包含了try-catch等语句块。去掉了类层次,代码和上面完全一样。误区六、用Exception来捕获所有潜在的异常在一个方法的执行过程中会抛出几种不同类型的异常。为了代码简洁,使用基类Exception来捕获所有潜在的异常,如下例所示:清单7publicvoidretrieveObjectById(Longid){try{//...抛出IOException的代码调用//...代码调用thatthrowsSQLException}catch(Exceptione){//这里使用了基类Exception捕获的所有潜在异常。如果这样捕获到多层,就会丢失原来异常的有效信息thrownewRuntimeException("ExceptioninretieveObjectById",e);}}可以重构到list8publicvoidretrieveObjectById(Longid){try{//..somecodethatthrowsRuntimeException,IOException,SQLException}catch(IOExceptione){//只捕获IOExceptionthrownewRuntimeException(/*这里指定IOException对应的错误码*/code,"ExceptioninretieveObjectById",e);}catch(SQLExceptione){//只捕获SQLExceptionthrownewRuntimeException(/*这里指定SQLException对应的错误码*/code,"ExceptioninretieveObjectById",e);}}误区7.多级封装抛出不可检测的异常。如果我们总是坚持不同类型的异常必须使用不同的catch语句,那么大部分的例子都可以绕过这一节。但是如果一次代码调用抛出不止一种异常,往往没有必要为每种不同类型的异常都写一个catch语句。对于开发来说,任何一种异常都足以说明程序的具体问题。List9try{//MaythrowRuntimeException,IOExeptionorothers;//注意这个和误区6的区别,这里是一段抛出各种异常的代码。上面是多段代码,每段代码都会抛出不同的异常}catch(Exceptione){//一如既往的将Exception转换成RuntimeException,但是这里的e其实是RuntimeException的一个实例,在前面的代码中已经封装了thrownewRuntimeException(/**/code,/**/,e);}如果我们像上面的例子一样把所有的Exception都转成RuntimeException,那么当Exception的类型已经是RuntimeException时,再做一次封装。RuntimeException被重新封装,原来的RuntimeException携带的有效信息丢失了。解决方法是我们可以在RuntimeException类中加入相关检查,确认参数Throwable不是RuntimeException的实例。如果是,相应的属性将被复制到新创建的实例中。或者使用不同的catch语句块来捕获RuntimeException和其他异常。个人喜好方法1、好处不言而喻。误区八、多级打印异常我们来看下面的例子,定义了两个类A和B,其中B类的代码在A类中被调用,异常在A类和B类中都被捕获并打印B类。清单10publicclassA{privatestaticLoggerlogger=LoggerFactory.getLogger(A.class);publicvoidprocess(){try{//实例化B类,可以换成其他注入方式bb=newB();b.process();//othercodemightcauseexception}catch(XXXExceptione){//如果B类的process方法抛出异常,异常会在B类打印,这里也会打印,所以logger.error(e)会打印两次;thrownewRuntimeException(/*错误码*/errorCode,/*异常信息*/msg,e);}}}publicclassB{privatestaticLoggerlogger=LoggerFactory.getLogger(B.class);publicvoidprocess(){try{//可能抛出的代码exception}catch(XXXExceptione){logger.error(e);thrownewRuntimeException(/*ErrorCode*/errorCode,/*ExceptionInformation*/msg,e);}}}同一个异常会打印两次。如果级别再复杂一点,在不考虑打印日志消耗的系统性能的情况下,在异常日志中定位到具体问题就已经够头疼了。其实打印日志只需要在代码的最外层抓取打印,异常打印也可以写成AOP织入框架的最外层。误区9.异常中包含的信息不能完全定位问题。异常不应该仅仅让开发者知道问题出在哪里,更多的时候开发者需要知道是什么导致了问题。我们知道java.lang.Exception有一个string类型参数的构造方法,这个字符串可以自定义成通俗易懂的提示信息。简单的自定义信息开发者只能知道哪里发生了异常,但是很多时候,开发者需要知道是什么参数导致了这样的异常。这时候我们需要在自定义信息中加入方法调用的参数信息。下面的例子只列出了一个参数的情况。在有多个参数的情况下,可以写一个工具类来组织这样一个字符串。清单11publicvoidretrieveObjectById(Longid){try{//..somecodethatthrowsSQLException}catch(SQLExceptionex){//在异常信息中添加参数信息thrownewRuntimeException("ExceptioninretieveObjectByIdwithObjectId:"+id,ex);}}误区十、不可预测的潜在Exception在写代码的过程中,由于对调用代码了解不深入,无法准确判断被调用代码是否会产生异常,所以忽略了处理。产生ProductionBug后才想到应该在某段代码中加入exceptioncatch,甚至无法准确指出异常的原因。这就要求开发者不仅要知道自己在做什么,还要尽可能多地知道别人做了什么以及可能导致什么结果,并从全局的角度来考虑整个申请流程。这些想法会影响我们编写和处理代码的方式。误区十一、混用多个第三方日志库现在Java第三方日志库的种类越来越多。一个大的项目会引入各种框架,而这些框架又会依赖于不同日志库的实现。.最头疼的问题是没有引入所有需要的日志库。问题是导入的日志库相互不兼容。如果在项目前期可能比较容易解决,可以根据需要在所有代码中重新引入日志库,或者换一个框架。但这样的成本并不是每个项目都能承受得起的,而且项目越往前推进,风险就越大。如何才能有效避免类似问题?目前的大部分框架都考虑到了类似的问题。您可以通过配置Properties或xml文件、参数或在运行时扫描Lib库中的日志来实现类,从而真正运行应用程序。仅当您确定要应用哪个特定日志记录库时。其实根据不需要多级打印日志的原则,我们可以简化很多原本调用日志打印代码的类。很多时候,我们可以使用拦截器或者过滤器来打印日志,减少代码维护和迁移的成本。结论以上纯属个人经验和总结,一切都是辩证的,没有绝对的原则,适合自己的才是最有效的原则。希望以上的解??释和分析能对您有所帮助。
