JAVA异常设计原则

异常是面向对象语言非常重要的一个特性,良好的异常设计对程序的可扩展性、可维护性、健壮性都起到至关重要。JAVA根据用处的不同,定义两类异常 *Checked Exception:Exception的子类,方法签名上需要显示的声明throws,编译器迫使调用者处理这类异常或者声明throws继续往上抛。 *Unchecked Exception:RuntimeException的子类,方法签名不需要声明throws,编译器也不会强制调用者处理该类异常。异常的作用和好处:1. 分离错误代码和正常代码,代码更简洁。2. 保护数据的正确性和完整性,程序更严谨。3. 便于调试和排错,软件更好维护。……相信很多JAVA开发人员都看到或听到过“不要使用异常来控制流程”,虽然这句话非常易于记忆,但是它并未给出“流程”的定义,所以很难理解作者的本意,让人迷惑不解。如果“流程”是包括程序的每一步执行,那异常就是用来控制流程的,它就是用来区分程序的正常流程和错误流程,为了更能明确的表达意思,上面这句话 应改成“不要用异常来控制程序的正常流程”。现在带来一个新的问题:如何区分程序正常流程和异常流程?我实在想不出一个评判标准,就举例来说明,大家思维 扩散下。为了后面更方便的表达,我把异常分成两类,不妥之处请谅解 *系统异常:软件的缺陷,客户端对此类异常是无能为力的,通常都是Unchecked Exception。 *业务异常:用户未按正常流程操作导致的异常,都是Checked Exception一个金币转账的例子:需求规定金币一次的转账范围是1~500,如果超过这个额度,就要提示用户金额超出单笔转账的限制。现在有以下几种场景:1. 转账的金额是由用户在页面随意输入的:因为值是用户随意输入的,所以给的值超出限定的范围肯定是司空见惯。我们当然不能把它(输入的值超出限定的范围)归结于异常流程,它应该属于正常流程。正确的实现如下:提供一个判断转账金币数量是否超出限定范围的方法

Java代码

    privatestaticfinalintMAX_PRE_TRANSFER_COIN=500;publicbooleanisCoinExceedTransferLimits(intcoin){returncoin>MAX_PRE_TRANSFER_COIN;}

Action里先对值进行判断,若不合法,直接返回并提示用户2. 转账的额度是页面单选框(100、200、300、400、500)选择的:转账的额度用户不能轻易的更改,但是不排除有些无聊的人会借助其他工具(如,firebug)来修改。此类事件还是占少数的,我们就可以把它归结为非软件bug的异常事件—业务异常(Checked Exception)。正确的实现如下CoinExceedTransferLimitExcetion.java

Java代码

    //金币超出限定范围的异常类publicclassCoinExceedTransferLimitExcetionextendsException{privatestaticfinallongserialVersionUID=-7867713004171563795L;privateintcoin;publicCoinExceedTransferLimitExcetion(){}publicCoinExceedTransferLimitExcetion(intcoin){this.coin=coin;}publicintgetCoin(){returncoin;}@OverridepublicStringgetMessage(){returncoin+"isexceedtransferlimit:500";}}

//转账方法

Java代码

    privatestaticfinalintMAX_PRE_TRANSFER_COIN=500;publicvoidtransferCoin(intcoin)throwsCoinExceedTransferLimitExcetion{if(coin>MAX_PRE_TRANSFER_COIN)thrownewCoinExceedTransferLimitExcetion(coin);//dotransferingcoin}

3. 接口transferCoin(int coin)的规范里已经定了契约,调用transferCoin之前必须要先调用isCoinExceedTransferLimits判断值是否合法:虽然规范人人都要遵循,但毕竟只是规范,编译器无法强制约束。此时就需要用系统异常(Unchecked Exception)来保证程序的正确性,没遵守规范的就当做软件bug处理。正确的实现如下:

Java代码

    publicclassCoinExceedTransferLimitExcetionextendsRuntimeException{privatestaticfinallongserialVersionUID=-7867713004171563795L;publicCoinExceedTransferLimitExcetion(){}publicCoinExceedTransferLimitExcetion(intcoin){super(coin+"isexceedtransferlimit:500");}}

//转账方法

Java代码

    publicvoidtransferCoin(intcoin){if(coin>MAX_PRE_TRANSFER_COIN)thrownewCoinExceedTransferLimitExcetion(coin);//dotransferingcoin}

至此,举例已经结束了,在这里再延伸下—业务异常和系统异常类在实现上的区别,该区别的根源来自于调用者捕获到异常后是如何处理的Action对业务异常的处理:操作具体的异常类

Java代码

    publicStringexecute(){try{userService.transferCoin(coin);}catch(CoinExceedTransferLimitExcetione){e.getCoin();}returnSUCCESS;}

Action对系统异常的处理:无法知道具体的异常类

Java代码

    publicStringexecute(){try{userService.transferCoin(coin);}catch(RuntimeExceptione){LOG.error(e.getMessage());}returnSUCCESS;}

调用者捕获业务异常(Checked Excetion)之后,通常不会去调用getMessage()方法的,而是调用异常类里特有的方法,所以业务异常类的实现要注重特有的,跟业务相关的方法,而不是getMessage()方法。系统异常类恰恰相反,捕获者只会调用getMessage()获取异常信息,然后记录错误日志,所以系统异常类(Uncheck Exception)的实现类对getMessage()方法返回内容比较讲究。不管是业务异常还是系统异常,都需要提供丰富的信息。如:数据库访问异常,需要提供查询sql语句等;HTTP接口调用异常,需要给出访问的URL和参数列表(如果是post请求,参数列表不提供也可以,取决于维护人员或者开发人员拿到参数列表会如何去使用)。1. Sql语法错误异常类(取自spring框架的异常类,Spring的异常体系很强大,值得一看):

Java代码

    publicclassBadSqlGrammarExceptionextendsInvalidDataAccessResourceUsageException{privateStringsql;publicBadSqlGrammarException(Stringtask,Stringsql,SQLExceptionex){super(task+";badSQLgrammar["+sql+"]",ex);this.sql=sql;}publicSQLExceptiongetSQLException(){return(SQLException)getCause();}publicStringgetSql(){returnthis.sql;}}

2. HTTP接口调用异常类

Java代码

    publicclassHttpInvokeExceptionextendsRuntimeException{privatestaticfinallongserialVersionUID=-6477873547070785173L;publicHttpInvokeException(Stringurl,Stringmessage){super("httpinterfaceunavailable["+url+"];"+message);}}

如何选择用Unchecked Exception和Checked Exception1.是软件bug还是业务异常,软件bug是Unchecked Exception,否则是Checked Exception2.如果把该异常抛给用户,用户能否做出补救。如果客户端无能为力,则用Unchecked Exception,否则抛Checked Exception结合这两点,两类异常就不会混淆使用了。异常设计的几个原则:如果方法遭遇了一个无法处理的意外情况,那么抛出一个异常。避免使用异常来指出可以视为方法的常用功能的情况。如果发现客户违反了契约(例如,传入非法输入参数),那么抛出非检查型异常。如果方法无法履型契约,那么抛出检查型异常,也可以抛出非检查型异常。如果你认为客户程序员需要有意识地采取措施,那么抛出检查型异常。异常类应该给客户提供丰富的信息,异常类跟其它类一样,允许定义自己的属性和方法。异常类名和方法遵循JAVA类名规范和方法名规范跟JAVA其它类一样,不要定义多余的方法和变量,不会使用的变量,就不要定义。我觉得BadSqlGrammarException.getSql() 就是多余的以下是我工作当中碰到的一些我认为不是很好的写法,我之前也犯过此类的错误A. 整个业务层只定义了一个异常类

Java代码

    publicclassServiceExceptionextendsRuntimeException{privatestaticfinallongserialVersionUID=8670135969660230761L;publicServiceException(Exceptione){super(e);}publicServiceException(Stringmessage){super(message);}}

理由:1.业务异常不应该是Unchecked Exception。2.不存在一个具体的异常类名称是“ServiceException”。解决方法:定义一个抽象的业务异常“ServiceException”

Java代码

    publicabstractclassServiceExceptionextendsException{privatestaticfinallongserialVersionUID=-8411541817140551506L;}

B. 忽略异常

Java代码

    try{newString(source.getBytes("UTF-8"),"GBK");}catch(UnsupportedEncodingExceptione){e.printStackTrace();}

理由:1.环境不支持UTF-8或者GBK很显然是一个非常严重的bug,不能置之不理2.堆栈的方式记录错误信息不合理,若产品环境是不记录标准输出,这个错误信息就会丢失掉。若产品环境是记录标准输出,万一这段程序被while循环的线程调用,有可能引起硬盘容量溢出,最终导致程序的运行不正常,甚至数据的丢失。解决方法:捕获UnsupportedEncodingException,封装成Unchecked Exception,往上抛,中断程序的执行。

Java代码

    try{newString(source.getBytes("UTF-8"),"GBK");}catch(UnsupportedEncodingExceptione){thrownewIllegalStateException("thebaseruntimeenvironmentdoesnotsupport’UTF-8’or’GBK’");}

C. 捕获顶层的异常—Exception

Java代码

    publicvoidtransferCoin(intoutUid,intinUserUid,intcoin)throwsCoinNotEnoughException{finalUseroutUser=userDao.getUser(outUid);finalUserinUser=userDao.getUser(inUserUid);outUser.decreaseCoin(coin);inUser.increaseCoin(coin);try{//BEGINTRANSACTIONuserDao.save(outUser);userDao.save(inUser);//ENDTRANSACTION//logtransferoperate}catch(Exceptione){thrownewServiceException(e);}}

理由:1. Service并不是只能抛出业务异常,Service也可以抛出其他异常如IllegalArgumentException、ArrayIndexOutOfBoundsException或者spring框架的DataAccessException2. 多数情况下,Dao不会抛Checked Exception给Service,假如所有代码都非常规范,Service类里不应该出现try{}catch代码。解决方法:删除try{}catch代码

Java代码

    publicvoidtransferCoin(intoutUid,intinUserUid,intcoin)throwsCoinNotEnoughException{finalUseroutUser=userDao.getUser(outUid);finalUserinUser=userDao.getUser(inUserUid);outUser.decreaseCoin(coin);inUser.increaseCoin(coin);//BEGINTRANSACTIONuserDao.save(outUser);userDao.save(inUser);//ENDTRANSACTION//logtransferoperate}

D. 创建没有意义的异常

Java代码

    publicclassDuplicateUsernameExceptionextendsException{}

理由1. 它除了有一个"意义明确"的名字以外没有任何有用的信息了。不要忘记Exception跟其他的Java类一样,客户端可以调用其中的方法来得到更多的信息。解决方案:定义上捕获者需要用到的信息

Java代码

    publicclassDuplicateUsernameExceptionextendsException{privatestaticfinallongserialVersionUID=-6113064394525919823L;privateStringusername=null;privateString[]availableNames=newString[0];publicDuplicateUsernameException(Stringusername){this.username=username;}publicDuplicateUsernameException(Stringusername,String[]availableNames){this(username);this.availableNames=availableNames;}publicStringrequestedUsername(){returnthis.username;}publicString[]availableNames(){returnthis.availableNames;}}

E. 把展示给用户的信息直接放在异常信息里。

Java代码

    publicclassCoinNotEnoughException2extendsException{privatestaticfinallongserialVersionUID=4724424650547006411L;publicCoinNotEnoughException2(Stringmessage){super(message);}}publicvoiddecreaseCoin(intforTransferCoin)throwsCoinNotEnoughException2{if(this.coin<forTransferCoin)thrownewCoinNotEnoughException2("金币数量不够");this.coin-=forTransferCoin;}

理由:展示给用户错误提示信息属于文案范畴,文案易变动,最好不要跟程序混淆一起。解决方法:错误提示的文案统一放在一个配置文件里,根据异常类型获取对应的错误提示信息,若需要支持国际化还可以提供多个语言的版本。F. 方法签名声明了多余的throws理由:代码不够精简,调用者不得不加上try{}catch代码解决方案:若方法不可能会抛出该异常,那就删除多余的throwsG. 给每一个异常类都定义一个不会用到ErrorCode理由:多一个功能就多一个维护成本解决方法:不要无谓的定义ErrorCode,除非真的需要(如给别人提供接口调用的,这时,最好对异常进行规划和分类。如1xx代表金币相关的异常,2xx代表用户相关的异常…最后推荐几篇关于异常设计原则1.异常设计http://www.cnblogs.com/JavaVillage/articles/384483.html(翻译)http://www.javaworld.com/jw-07-1998/jw-07-techniques.html(原文)2. 异常处理最佳实践http://tech.e800.com.cn/articles/2009/79/1247105040929_1.html(翻译)http://onjava.com/pub/a/onjava/2003/11/19/exceptions.html(原文)

多对自己说“我能行,我一定可以”,

JAVA异常设计原则

相关文章:

你感兴趣的文章:

标签云: