继续讨论Exception的处理
简述
用Java在开发系统的时候,Exception的处理往往是比较复杂的。如何处理开发中遇到的Exception,如何将合理的异常信息呈现给客户是开发人员必须要考虑的问题。
关于Exception的处理的文章在很多地方都可以看到,本文除了做一个总结之外,还将结合Design by Contract,JDK 1.4引入的assertion,以及如何用Spring的AOP处理Exception做进一步的探讨。
Exception的分类
从JDK的API中我们可以看到,Java把异常分为了Error和Exception两大类,在Exception中又分为checked exception和runtime exception。从系统开发的角度上,我们可以把exception分为:
· JVM异常。这种异常我们不应该捕捉,因为它的出现意味着一些比较严重的错误,比如OutOfMemoryError,StackOverflowError等;
· 系统异常。大多数情况下,系统异常以RuntimeException的形式出现,比如NullPointerException, ArrayOutOfBoundsException等,这时往往意味着我们的程序里面出现了Bug;还有一种情况,例如我们没有办法通过JNDI找到某个资源,也应该属于系统异常。系统异常的主要特点是,当我们遇到这种异常的时候,我们没有合适的办法处理,或者说我们不能已一个合理的方式告诉最终用户系统出现了什么错误。很难想象用户看到一个NPE,并在界面上看到一堆stack trace是什么感觉。这种异常应该在单元测试以及集成测试的时候被检测到,在发布的时候应该尽可能不出现这样的问题;
· 应用异常。这种异常是由我们的系统。这些异常的出现对用户来说,可能是因为某个验证没有通过,某个操作的步骤出现错误等,比如插入数据库的时候出现主键重复的情况等。总之,这些异常的信息可以通过一个用户看的懂的方式显示给用户。
Design By Contract(DBC)
我们暂时把Exception的处理放在一边,先看看Design by Contract的概念。
对于任何一个软件系统来说,一个重要的目标就是可靠性,即正确性和健壮性。系统的正确性主要看这个系统是不是符合Specificatoin,健壮性主要是指当遇到Specification没有涉及的情况,即异常情况的时候能不能以一种合理的方式解决。
DBC的主要思想是一个类和它的客户程序之间有一个合同;客户程序必须保证调用这个类之前某些前提条件必须满足(Precondition),而这个类必须保证在被调用之后的某些属性和状态是正确的(Postcondidtion/Class Invariants)。如果能有一种方式能够让编译器检查这些Precondition和Postcondition是否正确,这个合同是否被满足,那么出现的错误可以被立即捕获。
例如,一个类需要一个setMonth( int month )的方法,我们一般的实现方法大致如下:
public void setMonth( int month )
{
if( month > 0 && month < 13 )
{
throw new IllegalArgumentException( “” );
}
this.month = month;
}
但是按照DBC的概念,应该由客户代码,而不是setMonth方法保证传进来的参数是一个正确的数值,而setMonth应该保证当方法执行之后,month的值被正确设置,同时保证该类处在一个正确的状态。所以setMonth中对于参数month的验证就不在这里出现了。从这个例子中可以看到,DBC的引入对不同类的责任有一个明确的划分,原来的代码里面setMonth的责任现在被转移到了客户代码里面。
目前,编程语言中对DBC支持较好的是Eiffel,而在JDK1.4中引入的assertion则为Java在DBC方面提供了一些支持。
Java Assertion
Java虽然不直接支持DBC,但我们可以利用JDK1.4提供的assertion功能做一些这方面的工作。下面简单介绍assertion的用法,详细的信息请参见sun网站上的资料(http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html)。
Assert有两种用法
· assert BooleanExpression和
· assert BooleanExpression : DetailMessage
系统运行的时候如果检查到BooleanExpression是false,那么就是抛出一个AssertionError。DetailMessage如果提供,会通过AssertionError的构造函数传进去。
由于assertion是在JDK1.4引入的,为了编译包含有assertion应用的Java程序,我们需要在javac中打开开关source=”1.4”,如在ant的build文件中,我们一般这么写:
<javac srcdir=”${src.dir}” destdir=”${build.dir}” source=”1.4” target=”1.4”>
<classpath refid=”class.path”/>
</javac>
而在IDE环境中注意做相应的变化。
在缺省情况下,assertion在运行时是被禁用的,我们通过开关-ea和-da来打开和关闭assertion的应用,如:
java –ea SomeClass
有了assertion的帮助,我们在上面的assertion代码中做如下的改变(注意:请参见Sun的assertion文档,这里仅仅示例assertion的用法,并没有考虑assertion的best practice中的某些原则):
public void setMonth( int month )
{
assert month > 0 && month < 13;
this.month = month;
//assert the state of this class in valid.
}
在开发之前,我们和客户代码先签订合同,我们这里在调用方法之前先检查precondition是不是已经满足,如果没有满足,我们就直接抛出AssertionError,因为我们合同里面已经做了约定,输入参数的正确性应该由客户保证(第一个assertion不应该出错),而我们这里应该保证方法被调用之后的postcondition和class invariants的正确性。
引入了assertion,我们一般在开发过程中应该打开-ea开关,这样可以在单元测试的时候能够捕捉到相当数量的自己的bug,从而保证系统的健壮。
Java Assertion Best Practice
Sun的assertion使用文档上对什么时候使用assertion做了指导,对其中的一点,我们存在一定的疑问,认为Sun的提法稍显武断(谁这么胆大包天,竟敢质问Sun的伟大J).
[Do not use assertions to check the parameters of a public method.]
在我们实际开发的过程中,我们认为,Sun这里提到的public method应该理解为不同模块之间的接口,而不是单纯意义上的Java类的公用方法。假设一个小组提供一个底层支持模块给其他小组使用,而这个小组内部也分了很多层次,那么这里的Public method我们认为应该指提供给其他模块/组使用的API,而内部的某些方法,虽然是公用的,仍然可以使用assertion,因为这个合同属于是内部合同。所以我们认为这里的public method应该指存在合同关系的两个模块之间接口的public method。
另外,assertion的使用在概念会和数据验证出现一定的重叠,这个我们需要根据实际的情况决定什么时候使用assertion。
Exception的处理
回到正题,根据上面的讨论,对于三种Exception来说,JVM的Exception我们在系统实现上不予考虑,对于一部分系统异常来说,我们可以使用assertion解决掉一部分由于系统的bug引起的异常,对于其余的Exception要正确处理,而处理的标准则是一方面能够给最终用户提供有意义的出错信息,另外一方面,由于尽管我们做了认真的测试,我们仍不能保证系统在发布之后不会出现任何问题,因此需要考虑能够有合理的方法准确定位错误,为开发人员纠错提供依据。
在实际的应用中,关于Exception类的设计我们可以借鉴一下Spring中关于对Exception的设计。通过考察Spring DAO支持中Exception的设计我们可以发现以下几个特点:
1. 所有的Exception都来自一个跟节点;
2. 包装如SQLException等异常,使错误信息没有丢失;
3. 采用RuntimeException,而不是checked exception。
在学习Spring的过程中,刚开始没有完全理解采用RuntimeException的好处,但通过对其AOP支持的理解,的确觉得采用RuntimeException有一定的道理:
1. 我们不用在一个方法后面声明这个方法需要throws XXXException;
2. 在调用这个方法的时候也不同一定要try…catch段了;
3. EJB的CMT中,也是靠抛出一个RuntimeException,EJBException,来通知容易回滚事务,Spring用AOP管理事务的方法和它有了相通的地方(虽然Spring的事务管理中没有规定必须要抛出RumtimeException才能回归事务)
使用RuntimeException并没有限制我们在开发过程中一定不捕获这些异常,如果一个系统中,上层模块需要捕获下层模块抛出的异常,然后在其中增加信息,RuntimeException并没有限制我们这么做,相反,因为开发人员必须更加了解底层模块的情况,而不是借助现在方便的开发工具如Eclipse或者JBuilder自动生成try…catch代码段,使得我们的系统可能出错的机会得到一定程度的降低。
AOP中的throws advice
这里把这个题目加进来是因为AOP可能会给我们的设计带来一些跟以往设计不同的地方,所以这里提供一个Spring的throws advice的实例来看看AOP会给我们带来一些什么样的变化。
假设我们的系统需要记录详细的出错信息日志,按照以往的想法,我们会在系统中所有抛出异常的地方加上记录日志的代码;但是引入了AOP之后,我们可以将出错和记录日志两个模块完全解耦,通过Spring配置的方法将两个模块结合在一起。
请看下面的代码:
//定义一个Service,
Public interface ItestService {
Void doSomething();
}
Public class TestServiceImpl implements ITestService {
Public void doSomehting() {
Throw new RuntimeException(…);
}
}
//定义throws advice
Public class LogAdvice implements ThrowsAdvice {
Public void afterThrowing( RuntimeException ex ) throws Throwable
{
//Log the error information
}
}
//Spring的配置文件
<bean id="throwsAdvisor" class="com.company.LggAdvice"/>
<bean id="testService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<bean class="com.company.TestServiceImpl"/>
</property>
<property name="proxyInterfaces">
<value>com.company.ITestService</value>
</property>
<property name="interceptorNames">
<list>
<value>throwsAdvisor</value>
</list>
</property>
</bean>
在系统运行的时候,当TestService的方法抛出异常的时候,LogAdvice中的afterThrowing方法就会被调用;同时这个advisor并不吃掉这个异常,而是继续抛出去,从而不会影响我们原来的流程。从中我们看到,两个模块之间的耦合从原来的代码中转移到了配置文件中,因此我们的设计可以充分利用AOP给我们带了的优势。
参考文档:
1. http://www.javaworld.com/javaworld/jw-02-2002/jw-0215-dbcproxy-p2.html
2. http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html
3. http://www.springframework.org/docs/reference/index.html