zoukankan      html  css  js  c++  java
  • JAVA异常设计原则

    异常是面向对象语言非常重要的一个特性,良好的异常设计对程序的可扩展性、可维护性、健壮性都起到至关重要。 

    JAVA根据用处的不同,定义了两类异常 
        * Checked Exception: Exception的子类,方法签名上需要显示的声明throws,编译器迫使调用者处理这类异常或者声明throws继续往上抛。 
        * Unchecked Exception: RuntimeException的子类,方法签名不需要声明throws,编译器也不会强制调用者处理该类异常。 

    异常的作用和好处: 
    1. 分离错误代码和正常代码,代码更简洁。 
    2. 保护数据的正确性和完整性,程序更严谨。 
    3. 便于调试和排错,软件更好维护。 
    …… 

    相信很多JAVA开发人员都看到或听到过“不要使用异常来控制流程”,虽然这句话非常易于记忆,但是它并未给出“流程”的定义,所以很难理解作者的本意,让人迷惑不解。 

    如果“流程”是包括程序的每一步执行,我认为异常就是用来控制流程的,它就是用来区分程序的正常流程和错误流程,为了更能明确的表达意思,上面这句话应改成“不要用异常来控制程序的正常流程”。现在带来一个新的问题就是如何区分程序正常流程和异常流程?我实在想不出一个评判标准,就举例来说明,大家思维扩散下。 

    为了后面更方便的表达,我把异常分成两类,不妥之处请谅解 

        * 系统异常:软件的缺陷,客户端对此类异常是无能为力的,通常都是Unchecked Exception。 
        *业务异常:用户未按正常流程操作导致的异常,都是Checked Exception 

    金币转帐例子 
    1. 需求规定金币一次的转账范围是1~500,如果超过这个额度,就要提示用户金额超出单笔转账的限制,转账的金额是由用户在页面输入的: 
    因为值是用户输入的,所以给的值超出限定的范围肯定是司空见惯。我们当然不能把它(输入的值超出限定的范围)归结于异常流程,它应该属于正常流程,应该提供验证数据的完整功能。 
    正确的实现如下: 
    提供一个判断转账金币数量是否超出限定范围的方法 

    Java代码  收藏代码
    1. private static final int MAX_PRE_TRANSFER_COIN = 500;  
    2.   
    3. public boolean isCoinExceedTransferLimits(int coin) {  
    4. return coin > MAX_PRE_TRANSFER_COIN;  
    5. }  



    Action里先对值进行校验,若不合法,直接返回并提示用户 

    2. 在转账的过程里,发些金币数量不够: 
    我们的程序都是运行在并发环境中,Action无法完全做到判断金币是否足够。因为在判断之后和事务之前的刹那间,有可能产生其他扣费操作导致金币不够。这时我们就需要用业务异常(Checked Exception)来控制 

    正确的实现如下 
    CoinNotEnoughExcetion .java 

    Java代码  收藏代码
    1. //金币不够的异常类  
    2. public class CoinNotEnoughExcetion extends Exception {  
    3. private static final long serialVersionUID = -7867713004171563795L;  
    4. private int coin;  
    5. public CoinNotEnoughExcetion() {  
    6. }  
    7.   
    8. public CoinNotEnoughExcetion(int coin) {  
    9. this.coin = coin;  
    10. }  
    11.   
    12. public int getCoin() {  
    13. return coin;  
    14. }  
    15.   
    16. @Override  
    17. public String getMessage() {  
    18. return coin + " is exceed transfer limit:500";  
    19. }  
    20. }  


    //转账方法 

    Java代码  收藏代码
    1. private static final int MAX_PRE_TRANSFER_COIN = 500;  
    2.   
    3. public void transferCoin(int coin) throws CoinNotEnoughExcetion{  
    4. if (!hasEnoughCoin())  
    5. throw new CoinNotEnoughExcetion(coin);  
    6. // do transfering coin  
    7. }  




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

    //转账方法 

    Java代码  收藏代码
    1. public void transferCoin(int coin){  
    2. if (coin > MAX_PRE_TRANSFER_COIN)  
    3. throw new IllegalArgumentException(coin+" is exceed tranfer limits:500");  
    4. // do transfering coin  
    5. }  



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

    Action对业务异常的处理:操作具体的异常类 

    Java代码  收藏代码
    1. public String execute() {  
    2. try {  
    3. userService.transferCoin(coin);  
    4. catch (CoinExceedTransferLimitExcetion e) {  
    5. e.getCoin();  
    6. }  
    7. return SUCCESS;  
    8. }  



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

    Java代码  收藏代码
    1. public String execute() {  
    2. try {  
    3. userService.transferCoin(coin);  
    4. catch (RuntimeException e) {  
    5. LOG.error(e.getMessage());  
    6. }  
    7. return SUCCESS;  
    8. }  



    调用者捕获业务异常(Checked Excetion)之后,通常不会去调用getMessage()方法的,而是调用异常类里特有的方法,所以业务异常类的实现要注重特有的,跟业务相关的方法,而不是getMessage()方法。 
    系统异常类恰恰相反,捕获者只会调用getMessage()获取异常信息,然后记录错误日志,所以系统异常类(Uncheck Exception)的实现类对getMessage()方法返回内容比较讲究。 

    不管是业务异常还是系统异常,都需要提供丰富的信息。如:数据库访问异常,需要提供查询sql语句等;HTTP接口调用异常,需要给出访问的URL和参数列表(如果是post请求,参数列表不提供也可以,取决于维护人员或者开发人员拿到参数列表会如何去使用)。 
    1. Sql语法错误异常类(取自spring框架的异常类,Spring的异常体系很强大,值得一看): 

    Java代码  收藏代码
    1. public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException {  
    2. private String sql;  
    3.   
    4. public BadSqlGrammarException(String task, String sql, SQLException ex) {  
    5. super(task + "; bad SQL grammar [" + sql + "]", ex);  
    6. this.sql = sql;  
    7. }  
    8.   
    9. public SQLException getSQLException() {  
    10. return (SQLException) getCause();  
    11. }  
    12.   
    13. public String getSql() {  
    14. return this.sql;  
    15. }  
    16. }  



    2. HTTP接口调用异常类 

    Java代码  收藏代码
    1. public class HttpInvokeException extends RuntimeException {  
    2. private static final long serialVersionUID = -6477873547070785173L;  
    3.   
    4. public HttpInvokeException(String url, String message) {  
    5. super("http interface unavailable [" + url + "];" + message);  
    6. }  
    7. }  



    如何选择用Unchecked Exception和Checked Exception 
    1.是软件bug还是业务异常,软件bug是Unchecked Exception,否则是Checked Exception 
    2.如果把该异常抛给用户,用户能否做出补救。如果客户端无能为力,则用Unchecked Exception,否则抛Checked Exception 
    结合这两点,两类异常就不会混淆使用了。 


    异常设计的几个原则: 
    1.如果方法遭遇了一个无法处理的意外情况,那么抛出一个异常。 
    2.避免使用异常来指出可以视为方法的常用功能的情况。 
    3.如果发现客户违反了契约(例如,传入非法输入参数),那么抛出非检查型异常。 
    4.如果方法无法履型契约,那么抛出检查型异常,也可以抛出非检查型异常。 
    5.如果你认为客户程序员需要有意识地采取措施,那么抛出检查型异常。 
    6.异常类应该给客户提供丰富的信息,异常类跟其它类一样,允许定义自己的属性和方法。 
    7.异常类名和方法遵循JAVA类名规范和方法名规范 
    8.跟JAVA其它类一样,不要定义多余的方法和变量。(不会使用的变量,就不要定义,spring的BadSqlGrammarException.getSql() 就是多余的)
     

    以下是我工作当中碰到的一些我认为不是很好的写法,我之前也犯过此类的错误 
    A. 整个业务层只定义了一个异常类 

    Java代码  收藏代码
    1. public class ServiceException extends RuntimeException {  
    2. private static final long serialVersionUID = 8670135969660230761L;  
    3.   
    4. public ServiceException(Exception e) {  
    5. super(e);  
    6. }  
    7.   
    8. public ServiceException(String message) {  
    9. super(message);  
    10. }  
    11. }  



    理由: 
    1.业务异常不应该是Unchecked Exception。 
    2.不存在一个具体的异常类名称是“ServiceException”。 

    解决方法:定义一个抽象的业务异常“ServiceException” 

    Java代码  收藏代码
    1. public abstract class ServiceException extends Exception {  
    2. private static final long serialVersionUID = -8411541817140551506L;  
    3. }  



    B. 忽略异常 

    Java代码  收藏代码
    1. try {  
    2. new String(source.getBytes("UTF-8"), "GBK");  
    3. catch (UnsupportedEncodingException e) {  
    4. e.printStackTrace();  
    5. }  



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

    解决方法:捕获UnsupportedEncodingException,封装成Unchecked Exception,往上抛,中断程序的执行。 

    Java代码  收藏代码
    1. try {  
    2. new String(source.getBytes("UTF-8"), "GBK");  
    3. catch (UnsupportedEncodingException e) {  
    4. throw new IllegalStateException("the base runtime environment does not support 'UTF-8' or 'GBK'");  
    5. }  



    C. 捕获顶层的异常—Exception 

    Java代码  收藏代码
    1. public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {  
    2. final User outUser = userDao.getUser(outUid);  
    3. final User inUser = userDao.getUser(inUserUid);  
    4. outUser.decreaseCoin(coin);  
    5. inUser.increaseCoin(coin);  
    6. try {  
    7. // BEGIN TRANSACTION  
    8. userDao.save(outUser);  
    9. userDao.save(inUser);  
    10. // END TRANSACTION  
    11. // log transfer operate  
    12. catch (Exception e) {  
    13. throw new ServiceException(e);  
    14. }  
    15. }  


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

    解决方法:删除try{}catch代码 

    Java代码  收藏代码
    1. public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {  
    2. final User outUser = userDao.getUser(outUid);  
    3. final User inUser = userDao.getUser(inUserUid);  
    4. outUser.decreaseCoin(coin);  
    5. inUser.increaseCoin(coin);  
    6. // BEGIN TRANSACTION  
    7. userDao.save(outUser);  
    8. userDao.save(inUser);  
    9. // END TRANSACTION  
    10. // log transfer operate  
    11. }  



    D. 创建没有意义的异常 

    Java代码  收藏代码
    1. public class DuplicateUsernameException extends Exception {  
    2. }  


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

    解决方案:定义上捕获者需要用到的信息 

    Java代码  收藏代码
    1. public class DuplicateUsernameException extends Exception {  
    2. private static final long serialVersionUID = -6113064394525919823L;  
    3. private String username = null;  
    4. private String[] availableNames = new String[0];  
    5.   
    6. public DuplicateUsernameException(String username) {  
    7. this.username = username;  
    8. }  
    9.   
    10. public DuplicateUsernameException(String username, String[] availableNames) {  
    11. this(username);  
    12. this.availableNames = availableNames;  
    13. }  
    14.   
    15. public String requestedUsername() {  
    16. return this.username;  
    17. }  
    18.   
    19. public String[] availableNames() {  
    20. return this.availableNames;  
    21. }  
    22. }  


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

    Java代码  收藏代码
    1. public class CoinNotEnoughException2 extends Exception {  
    2. private static final long serialVersionUID = 4724424650547006411L;  
    3.   
    4. public CoinNotEnoughException2(String message) {  
    5. super(message);  
    6. }  
    7. }  
    8.   
    9. public void decreaseCoin(int forTransferCoin) throws CoinNotEnoughException2 {  
    10. if (this.coin < forTransferCoin)  
    11. throw new CoinNotEnoughException2("金币数量不够");  
    12. this.coin -= forTransferCoin;  
    13. }  



    理由:展示给用户错误提示信息属于文案范畴,文案易变动,最好不要跟程序混淆一起。 
    解决方法: 
    错误提示的文案统一放在一个配置文件里,根据异常类型获取对应的错误提示信息,若需要支持国际化还可以提供多个语言的版本。 

    F. 方法签名声明了多余的throws 
    理由:代码不够精简,调用者不得不加上try{}catch代码 
    解决方案:若方法不可能会抛出该异常,那就删除多余的throws 

    G. 给每一个异常类都定义一个不会用到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(原文)

  • 相关阅读:
    bzoj2733 永无乡 平衡树按秩合并
    bzoj2752 高速公路 线段树
    bzoj1052 覆盖问题 二分答案 dfs
    bzoj1584 打扫卫生 dp
    bzoj1854 游戏 二分图
    bzoj3316 JC loves Mkk 二分答案 单调队列
    bzoj3643 Phi的反函数 数学 搜索
    有一种恐怖,叫大爆搜
    BZOJ3566 概率充电器 概率dp
    一些奇奇怪怪的过题思路
  • 原文地址:https://www.cnblogs.com/liaoliao/p/5009154.html
Copyright © 2011-2022 走看看