高效处理异常
Java开发人员做出的有关架构的最重要的决定之一便是如何使用Java异常模型。Java异常处理成为社区中讨论最多的话题之一。一些人认为Java语 言中的已检查异常(Checked Exceptions)是一次失败的尝试。本文认为错误并不在于Java模型本身,而在于Java库设计人员没有认识到方法失败的两个基本原因。本文提倡 思考异常情况的本质,并描述了有助于用户设计的设计模式。最后,本文讨论了异常处理在面向方面编程(Aspect Oriented Programming)模型中作为横切关注点(crosscutting concern)的情况。如果使用得当,Java异常将对程序开发人员大有裨益。本文将帮助读者正确使用Java异常。
为什么异常非常重要Java应用程序中的异常处理可以告诉用户构建应用程序的架构强度。架构是指在应用程序的各个层面上所做出的并始终遵守的决策。其中最重要的决 策之一便是应用程序中类、子系统或层之间进行互相通信的方式。方法通过Java异常可以为操作传递另一种结果,因此应用程序架构特别值得我们去关注。
判断Java架构师技能的高低和开发团队是否训练有素,其中比较好的方法是查看应用程序中的异常处理代码。首先需要观察的是有多少代码专门用于 捕捉异常、记录异常、确定发生的事件和异常转化。简洁、紧凑和有条理的异常处理表明团队有使用Java异常的一致方法。当异常处理代码的数量将要超过其他 方面的代码时,可以断定团队成员之间的沟通已经打破(或者这种沟通从一开始就不存在),每个人都用自己的方法来处理异常。
临时异常处理的结果非常具有预见性。如果问团队成员为什么在代码的某个特定点丢弃、捕捉、或忽略某个异常,回答通常是:“除此之外,我不知道怎 么做。”如果问他们在编写代码的异常实际发生时会产生什么情况,他们通常会皱眉,回答类似于:“我不知道。我们从来没有测试过。”
要判断Java组件是否有效地利用了Java异常,可以查看其客户端的代码。如果客户端代码中包含大量计算操作失败时间、原因和处理方法的逻 辑,原因几乎都是由于组件的错误报告设计。有缺陷的报告会在客户端产生大量的“记录和遗留”(log and forget)代码,而没有任何用途。最糟糕的是扭曲的逻辑路径、互相嵌套的try/catch/finally程序块,以及其他导致脆弱和无法管理的应 用程序的混乱。
事后处理异常(或者根本不处理)是造成软件项目混乱和延迟的主要原因。异常处理关系到软件设计的各个方面。为异常建立架构约定应该是项目中首先要做出的决定之一。合理使用Java异常模型将对保持应用程序的简洁性、可维护性和正确性大有帮助。
挑战异常准则如何正确使用Java异常模型已经成为大量讨论的主题。Java并不是支持异常语义的第一种语言;但是,通过Java编译器可强制使用规则来控 制如何声明和处理特定的异常。许多人认为编译时异常检查对精确软件设计有帮助,它与其他语言特征能够很好地协调起来。图1表明了Java异常的层次结构。
通常,Java编译器会根据java.lang.Throwable强制方法抛出异常,包括其声明中“throw”子句的异常。同样,编译器会 验证方法的客户端是捕获声明异常类型还是指定自己抛出异常类型。这些简单的规则对于全世界的Java开发人员产生了深远的影响。
编译器针对Throwable继承树的两个分支放宽了异常检查行为。java.lang.Error和 java.lang.RuntimeException的子类免于编译时检查。在两者中,软件设计人员通常对运行时异常更感兴趣。通常使用术语“未检查异 常”(unchecked exception)来区分其他的所有“已检查异常”(checked exception)
?
图 1 Java异常层次结构
我认为已检查异常会受到那些重视Java语言中强类型的人的欢迎。毕竟,由编译器对数据类型产生的约束会鼓励严格编码和精确思维。编译时类型检 查有助于防止在运行时产生难以应付的意外事件。编译时异常检查将起到相同的效果,提醒开发人员注意的是,方法会有潜在的其他结果需要进行处理。
在早期,推荐无论何处都尽量使用已检查异常,以便充分借助编译器生产出无差错软件。Java API库的设计人员显然赞成已检查异常准则,并广泛使用这些异常来模仿在库方法中发生的任何意外事件。在J2SE 5.1 API规范中,已检查异常类型仍然比未检查异常类型多,比例要超过二比一。
对于程序员来说,Java类库中的绝大多数公共方法好像为每一个可能的失败都声明了已检查异常。例如,java.io包对已检查异常IOException的依赖性特别大。至少63个Java库包直接或间接通过其数十个子类之一发布了此异常。
I/O失败很严重但也很少见。另外,程序代码通常没有能力从失败中恢复。Java程序员发现他们必须提供IOException和类似在简单的 Java库方法调用时可能发生的不可恢复的事件。捕捉这些异常只会打乱原有简洁的代码,因为捕捉块并不能改善此类情况。而不捕捉这些异常可能会更糟糕,因 为编译器要求将这些异常加入到方法所抛出的异常列表中。这公开了一些实现细节,优秀的面向对象设计自然会将这些细节隐藏起来。
这种无法成功的情况导致许多严重违反Java编程模式的异常处理。当今的程序员经常被告诫要提防这些情况。同样,在创建工作区方面也产生大量正确和错误的建议。
一些Java天才开始质疑Java已检查异常模型是否是一次失败的尝试。可以确定某个地方出了问题,但是这和Java语言中的异常检查无关。失败的原因是Java API设计人员的思考方式,即他们认为大多数失败情况都相同,并且可以用相同类型的异常来传达。
错误和意外事件假想金融应用软件中的CheckingAccount类。CheckingAccount属于客户,维护当前余额,并且可以接受存款,根据支票 接受终止支付命令,以及处理入帐的支票。CheckingAccount对象必须协调并发线程的访问,因为每一个线程都可以改变它的状态。 CheckingAccount的processCheck()方法将Check对象作为参数,从账户余额中正常扣除支票金额。但是调用 processCheck()的支票结算客户端必须为两类意外事件做好准备。首先,CheckingAccount 可能有为支票注册的终止支付命令。第二,账户中可能没有足够的资金来支付支票金额。
所以,processCheck()方法使用三种可能的方式来响应其调用者。正常的响应方式是支票得到处理,在方法签名中声明的结果返回给调用 服务。这两类意外事件响应代表了金融领域非常真实的情况,它们需要与支票结算客户端进行通信。所有这三种processCheck()响应都是为模仿典型 的支票账户行为而精心设计的。
在Java中表示意外事件响应的通常方法是定义两种异常,即StopPaymentException和 InsufficientFundsException。客户端忽略这两个异常是不正确的,因为在应用程序正常操作时会被抛出这两个异常。这两个异常有助 于表达方法的所有行为,和方法签名一样十分重要。
客户端可以轻松地处理这两类异常。如果终止支票的支付,客户端可以取得支票进行特殊处理。如果没有足够的资金,为支付此支票,客户端将从客户的储蓄帐户中转移资金,并重新尝试。
这些意外事件可以预见,它是使用CheckingAccount API的自然结果。它们并不表示软件或执行环境的失败。将这些异常条件与实际的失败对比,实际的失败是由于CheckingAccount类的内部实现细节问题造成的。
假设CheckingAccount在数据库中维持持久的状态并使用JDBC API进行访问。在该API中,几乎每一个数据库访问方法都有失败的可能性,但原因与CheckingAccount实现无关。例如,有人会忘记打开数据 库服务器、不小心拔下了网线,或改变了访问数据库的密码。
JDBC依靠单独的已检查异常SQLException来报告一切可能的错误。大多数错误都与数据库的配置、连接和硬件设备有关。 processCheck()方法并不能以有意义的方式处理这些异常条件。很遗憾,因为processCheck()至少知道它自己的实现方式。调用堆栈 中的上游方法能够处理这些问题的可能性会更小。
CheckingAccount示例解释了导致方法执行不能返回预期结果的两个基本原因。下面首先介绍一些描述性术语:
意外事件是一种可以预见的情况,要求方法做出某种响应,以便能够表达方法所期望的目的。方法的调用者预见这些情况并采取策略应付这些情况。
错误是一种计划外的情况,它阻止方法达到其预期目的,并且如果不引用方法的内部实现,则无法描述这种情况。
从这两个术语来看,终止支付命令和透支是processCheck()方法两个可能的意外事件。SQL问题代表了可能的错误异常条件。processCheck()的调用者应当提供一种处理意外事件的方式,但当错误发生时,并不能合理地处理该错误。
映射Java异常对于意外事件和错误,思考其原因将有助于为应用程序架构中的Java异常建立约定。
异常条件 意外事件 错误
图2 意外事件异常和错误异常
如果开发人员正在使用Spring MVC框架,简单地扩展SimpleMappingExceptionResolver并进行配置使其能处理RuntimeExceptio及其子类,便 能建立起错误屏障。通过重写resolveException()方法,在使用超类方法向发送普通错误显示的查看组件发出请求之前,开发人员可以添加任何 自定义处理。
当架构包含错误屏障并且开发人员也意识到了它的存在时,编写一劳永逸的错误异常处理代码的吸引力急剧下降。结果是在应用程序中产生更简洁和更易维护的代码。
架构中的意外事件处理随着错误处理委托给屏障,主要组件之间的意外事件通信变得更加简单。意外事件代表了另一种方法结果,此结果与主要返回结果同样重要。因此,已检 查异常类型是传递意外事件条件存在性并提供对付异常条件所需信息的良好工具。最佳实践是借助Java编译器来提醒开发人员他们正在使用API的所有方面, 同样需要提供方法结果的全部范围。
通过单独使用方法的返回类型,可以传递简单的意外事件。例如,返回null引用而非实际对象可以说明此对象由于明确的原因而无法创建。Java I/O方法通常返回整数值-1,而不是字节值或字节计数,用来表明文件的结束。如果方法的语义非常简单允许这样做,另一种返回值可以使用这种方式,因为它 们消除了由异常而带来的开销。不利方面是方法调用者负责检测返回值,来查看它是主要结果还是意外事件结果。然而,编译器并不强制调用者做这样的测试。
如果方法具有void返回类型,异常将是表明意外事件发生的唯一方法。如果方法返回对象引用,则返回值所表达的意思仅限于两个值(null和 non-null)。如果方法返回整数值,通过选择确保与主要返回值不相冲冲突的值,就可以表达数个意外事件条件。但是现在已经进入了错误代码检查的范 畴,这是Java异常模型需要避免的情况。
提供有用的信息定义不同的错误报告异常类型没有任何道理,因为错误屏障会对它们进行同样的处理。意外事件异常差异很大,因为它们会向方法调用者传达各种条件。您的架构可能明确指定这些异常都应该扩展java.lang.Exception或指定的基类。
不要忘记这些异常是完整的Java类型,可以调整特定的字段、方法以及为特殊目的而构建的构造函数。例如,假想的 CheckingAccount processCheck()方法抛出的InsufficientFundsException类型可能包括OverdraftProtection对 象,此对象能够转移资金以弥补另一个账户的资金短缺,此账户的身份取决于设置核算账户的方式。
记录还是不记录记录错误异常有实际意义是因为它们的目的是吸引开发人员去注意需要纠正的情况。但这并不适用于意外事件异常。它们可能代表相对少见的事件,但是在应用程序 的生命周期内,这些意外事件依然会发生。它们表明了如发生异常应用程序将按照其设计意图进行工作。按照惯例,在意外事件捕捉模块中加入记录代码只会增加混 乱代码而没有任何益处。如果意外事件表示重要事件,最好为方法产生一条记录项,用于在抛出意外事件异常并通知其调用者之前记录此事件。
异常方面在面向方面编程(Aspect Oriented Programming (AOP))中,错误和意外事件的处理是横切关注点。例如,要实现错误屏障模式,所有参与的类都必须遵守公共约定:
错误屏障方法必须驻留在遍历参与类的方法调用的头部。 它们都必须使用未检查异常来表示错误条件。 它们都必须使用特定的未检查异常类型,以便错误屏障能够接收到。 它们都必须从较低层方法中捕捉并转换已检查异常,这些异常在它们的执行环境中被视为错误。 它们不能干扰错误异常到屏障的传播。这些关注点跨越了其他不相关类的边界。结果产生了少量分散错误处理代码并致使屏障类与参与者之间的隐式耦合(尽管对于完全没有使用模式来说是一 次重大改进)。AOP允许将错误处理关注点封装到应用于参与类的公共Aspect中。Java AOP框架(比如AspectJ和Spring AOP)把异常处理作为联接点,错误处理行为(或建议)能够附加到上面。这样,在错误屏障模式中绑定参与者的惯例就有所放宽。错误处理现在可以存在于独立 的非内联方面(out-of-line aspect)中,避免了将“屏障”方法置于方法调用序列的前面。
如果开发人员在架构中使用AOP,错误和意外事件的处理是方面在整个应用程序中应用的理想候选对象。完全探究错误和意外事件处理在AOP中如何运作,这是个令人感兴趣的话题,留作以后讨论。
结束语尽管Java异常模型在其生命周期内已经引发了激烈的争论,但是当Java异常模型运用得当时,将会带来巨大的价值。作为架构师,应当决定如何 建立最大限度利用模型的惯例。思考一下错误和意外事件异常能够帮助开发人员做出正确的选择。Java异常模型使用得当,将保持应用程序的简洁性、可维护性 和正确性。把面向方面编程技术的错误和意外事件处理作为横切关注点,可为应用程序的架构带来某些明显的优势。
参考资料Sun's Exception Tutorial——关于Java异常的所有基本知识 Does Java Need Checked Exceptions?——Bruce Eckel反对Java中的已检查异常 Exceptional Java——有关这些话题的充分讨论和值得仿效的架构异常策略 The Exceptions Debate——来自developerWorks的有关异常的更多背景知识 The Apache Struts Web Application Framework——有关Struts的信息来源 The Spring Framework——有关Spring的信息来源 Wikipedia: Aspect Oriented Programming——对AOP概念的优秀介绍 The AspectJ Project——有关AspectJ的信息来源原文出处:http://dev2dev.bea.com/pub/a/2006/11/effective-exceptions.html