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

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

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

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

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

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

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

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

  1. private static final int MAX_PRE_TRANSFER_COIN = 500;
  2. public boolean isCoinExceedTransferLimits(int coin) {
  3. return coin > MAX_PRE_TRANSFER_COIN;
  4. }

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

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

正确的实现如下 
CoinNotEnoughExcetion .java

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

//转账方法

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

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

//转账方法

  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对业务异常的处理:操作具体的异常类

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

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

  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的异常体系很强大,值得一看):

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

2. HTTP接口调用异常类

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

如何选择用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. 整个业务层只定义了一个异常类

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

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

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

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

B. 忽略异常

  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,往上抛,中断程序的执行。

  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

  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代码

  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. 创建没有意义的异常

  1. public class DuplicateUsernameException extends Exception {
  2. }

理由 
1. 它除了有一个"意义明确"的名字以外没有任何有用的信息了。不要忘记Exception跟其他的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. public DuplicateUsernameException(String username) {
  6. this.username = username;
  7. }
  8. public DuplicateUsernameException(String username, String[] availableNames) {
  9. this(username);
  10. this.availableNames = availableNames;
  11. }
  12. public String requestedUsername() {
  13. return this.username;
  14. }
  15. public String[] availableNames() {
  16. return this.availableNames;
  17. }
  18. }

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

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

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

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(原文)

JAVA异常设计原则的更多相关文章

  1. 最简单直接地理解Java软件设计原则之开闭原则

    写在前面 本文属于Java软件设计原则系列文章的其中一篇,后续会继续分享其他的原则.想以最简单的方式,最直观的demo去彻底理解设计原则.文章属于个人整理.也欢迎大家提出不同的想法. 首先是一些理论性 ...

  2. Java六大设计原则

    类的设计原则     依赖倒置原则-Dependency Inversion Principle (DIP) 里氏替换原则-Liskov Substitution Principle (LSP) 接口 ...

  3. 有效处理Java异常三原则

    Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮.易于调试.异常之所以是一种强大的调试手段,在于其回答了以下三个问题: 什么出了错? 在哪出的错? 为什么出错? ...

  4. java面向对象设计原则

    原则1:DRY(Don't repeat yourself) 即不要写重复的代码,而是用"abstraction"类来抽象公有的东西.如果你需要多次用到一个硬编码值,那么可以设为公 ...

  5. 最简单直接地理解Java软件设计原则之单一职责原则

    理论性知识 定义 单一职责原则, Single responsibility principle (SRP): 一个类,接口,方法只负责一项职责: 不要存在多余一个导致类变更的原因: 优点 降低类的复 ...

  6. JAVA设计模式-设计原则

    6大原则: 单一职责原则 里氏替换原则 依赖倒置原则 接口隔离原则 迪米特法则 开闭原则 一.单一职责原则 定义:应该有且仅有一个原因引起类的变更 带来的好处: 类的复杂性降低,实现什么职责有清晰明确 ...

  7. Java API设计原则清单

    在设计Java API的时候总是有很多不同的规范和考量.与任何复杂的事物一样,这项工作往往就是在考验我们思考的缜密程度.就像飞行员起飞前的检查清单,这张清单将帮助软件设计者在设计Java API的过程 ...

  8. 浅谈Java五大设计原则之观察者模式

    定义一下观察者模式: 观察者模式又叫  发布-订阅  模式,定义的两个对象之间是一种一对多的强依赖关系,当一个对象的状态发生改变,所有依赖它的对象 将得到通知并自动更新(摘自Hand First). ...

  9. 最简单直接地理解Java软件设计原则之接口隔离原则

    理论性知识 定义 接口隔离原则, Interface Segregation Principle,(ISP). 一个类对应一个类的依赖应该建立在最小的接口上: 建立单一接口,不要建立庞大臃肿的接口: ...

随机推荐

  1. js私有化属性

    我们先来看一个例子: var Demo1 = function(val){ this.value = val; this.getValue = function(){ return this.valu ...

  2. 关于RSA加密

    RSA算法是一种非对称密码算法,所谓非对称,就是指该算法需要一对密钥,使用其中一个加密,则需要用另一个才能解密. RSA的算法涉及三个参数,n.e1.e2. 其中,n是两个大质数p.q的积,n的二进制 ...

  3. Table的分割线偏移量设置 及其 UIEdgeInset详解

    -(void)viewDidLayoutSubviews { if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)] ...

  4. 19.java.lang.NoClassDefFoundException

    java.lang.NoClassDefFoundException未找到类定义错误 当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误. 违背安全原则异常:Secturit ...

  5. [LeetCode][Python]14: Longest Common Prefix

    # -*- coding: utf8 -*-'''__author__ = 'dabay.wang@gmail.com'https://oj.leetcode.com/problems/longest ...

  6. MyEclipse修改

    MyEclipse设置编码方式 http://www.cnblogs.com/susuyu/archive/2012/06/27/2566062.html Eclipse添加Spket插件实现ExtJ ...

  7. UberX及以上级别车奖励政策(优步北京第四组)

    优步北京第四组: 定义为2015年7月20日至今激活的司机(以优步后台数据显示为准) 滴滴快车单单2.5倍,注册地址:http://www.udache.com/如何注册Uber司机(全国版最新最详细 ...

  8. LA 5966 Blade and Sword (双向bfs + 想法) - from lanshui_Yang

    题目大意:给你一张有n * m个网格的图,每个网格可能是如下符号: “#”:墙 “P”:出发点 “D”:终点 “.”:空地 “*”:传送机 有一个旅行家(假设名叫Mike),他要从点P到达点D,途中必 ...

  9. LCIS(线段树区间合并)

    LCIS Time Limit: 6000/2000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submi ...

  10. 什么是xss盲打

    什么是xss盲打? 盲打仅仅是一种惯称的说法,就是不知道后台不知道有没有xss存在的情况下,不顾一切的输入xss代码在留言啊,feedback啊之类的地方,尽可能多的尝试xss的语句与语句的存在方式, ...