`
cppmule
  • 浏览: 437148 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类

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(原文)
分享到:
评论

相关推荐

    Java异常处理的设计原则.pdf

    Java异常处理的设计原则

    深入探索高效的Java异常处理框架

    本文从Java异常最基本的概念、语法开始讲述了Java异常处理的基本知识,分析了Java异常体系结构,对比Spring的异常处理框架,阐述了异常处理的基本原则。并且作者提出了自己处理一个大型应用系统异常的思想,并通过...

    JAVA程序设计教程

    第一章程序和程序设计 .......................................................................................................1 §1.1 什么是程序 ?........................................................

    java课程设计指导书.doc

    其主要目的是使学生深入 理解并实践在本课程中 ,所学的有关JAVA 程序设计方面的语言基础、异常处理、事件处理、小应用程序以及图形用户界设计等。 本课程设计是为了配合Java 程序设计课程,帮助学生深入学习掌握...

    《Java程序设计案例教程》课程标准.doc

    课程 设置坚持以能力为本位的设计原则,倡导以学生为主体的教育理念,把提高学生的技术 应用能力放在重要位置,形成网络工程设计与施工、网络管理与维护、网站建设与管理 、信息类网络基础服务和网络信息安全建设五...

    JAVA 2平台安全技术-结构,API设计和实现

    7.1 设计原则 117 7.2 密码服务和服务提供器 118 7.3 密码类 121 7.3.1 java.security.Security 121 7.3.2 java.security.Provider 122 7.3.3 java.security.MessageDigest 122 7.3.4 java.security.Signature 123 ...

    Java 2平台安全技术-结构,API设计和实现

    7.1 设计原则 117 7.2 密码服务和服务提供器 118 7.3 密码类 121 7.3.1 java.security.Security 121 7.3.2 java.security.Provider 122 7.3.3 java.security.MessageDigest 122 7.3.4 java.security.Signature 123 ...

    JAVA上百实例源码以及开源项目

    例如,容易实现协议的设计。 Java EJB中有、无状态SessionBean的两个例子 两个例子,无状态SessionBean可会话Bean必须实现SessionBean,获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,计算利息等;...

    JAVA上百实例源码以及开源项目源代码

    例如,容易实现协议的设计。 Java EJB中有、无状态SessionBean的两个例子 两个例子,无状态SessionBean可会话Bean必须实现SessionBean,获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,计算利息等;...

    【白雪红叶】JAVA学习技术栈梳理思维导图.xmind

    设计原则 单一职责原则 开闭原则 里氏替换原则 依赖倒转原则 接口隔离原则 迪米特原则 设计模式 结构模式 适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 创建模式 抽象工厂...

    Java高级程序设计实战教程第一章-Java编码规范.pptx

    Java高级程序设计 第1章 Java编码规范 1.1 应用场景1.2 相关知识1.3 任务实施1.4 拓展知识1.5 拓展训练1.6课后小结 Java高级程序设计实战教程第一章-Java编码规范全文共13页,当前为第1页。 1.1 应用场景 一个大型的...

    java开源包11

    设计原则 容易维护扩展(不需要修改主类就可以添加新的API支持) 注入型解释器(依据不同的返回格式注入相应的解释器) 集中管理请求参数与参数映射 以运行时异常的方式来管理错误的响应 使用泛型来做强类型编程 多...

    java开源包6

    设计原则 容易维护扩展(不需要修改主类就可以添加新的API支持) 注入型解释器(依据不同的返回格式注入相应的解释器) 集中管理请求参数与参数映射 以运行时异常的方式来管理错误的响应 使用泛型来做强类型编程 多...

    java开源包9

    设计原则 容易维护扩展(不需要修改主类就可以添加新的API支持) 注入型解释器(依据不同的返回格式注入相应的解释器) 集中管理请求参数与参数映射 以运行时异常的方式来管理错误的响应 使用泛型来做强类型编程 多...

    java开源包101

    设计原则 容易维护扩展(不需要修改主类就可以添加新的API支持) 注入型解释器(依据不同的返回格式注入相应的解释器) 集中管理请求参数与参数映射 以运行时异常的方式来管理错误的响应 使用泛型来做强类型编程 多...

    java开源包4

    设计原则 容易维护扩展(不需要修改主类就可以添加新的API支持) 注入型解释器(依据不同的返回格式注入相应的解释器) 集中管理请求参数与参数映射 以运行时异常的方式来管理错误的响应 使用泛型来做强类型编程 多...

    Java面试宝典和毕设PPT模板

    作为准备进行Java程序员面试的候选人,以下是一些你应该关注和准备的关键领域和...面向对象设计和设计原则:理解面向对象设计的基本原则,如单一职责原则、开闭原则、依赖倒置原则等。能够使用UML图表进行简单的设计。

    达内java培训目录

    理解API设计原则;具备熟练的阅读API文档的能力;为后续的课程学习打下坚实的语言基础。 JavaSE核心 异常处理、多线程基础、IO系统、网络编程、Java反射机制、JVM性能调优(JVM内存结构剖析、GC分析及调优、JVM内存...

    java开源包5

    设计原则 容易维护扩展(不需要修改主类就可以添加新的API支持) 注入型解释器(依据不同的返回格式注入相应的解释器) 集中管理请求参数与参数映射 以运行时异常的方式来管理错误的响应 使用泛型来做强类型编程 多...

    Java2游戏编程.pdf

    附录B 一些常用的Java“要”与“不要”原则 附录C 使用JNI创建一个gamepad阅读器 附录D 使用.JAR文件来部署Java applet 附录E 正确运行Java 2 applet 附录F Magic游戏引擎源码清单 附录G 更多的资源 附录H 配书光盘...

Global site tag (gtag.js) - Google Analytics