有效的Java异常 |
|||||||||||||||||||
作者:Barry Ruzek 出处:dev2dev.bea.com.cn |
|||||||||||||||||||
意外事件异常条件完美地映射到Java已检查异常。由于它们是方法语义契约中不可或缺的一部分,因此必须借助编译器来确保问题得到了处理。如果开发人员坚持在编译器有问题时处理或声明意外事件异常,此时编译器会成为一种阻碍,可以断定此软件设计必须进行部分重构。这其实是一件好事。 错误条件对编程人员来说能够引起关注,而对于软件逻辑却并非如此。“软件诊断学家”收集错误信息以诊断和修复引起错误发生的根源。因此,未检查Java异常是错误的完美表现方式,它们可以使错误通知完整地过滤调用堆栈上的所有方法,传递到专门用于捕捉错误的层,捕获其中所包含的诊断信息,并为此活动提供一份受约束的合理结论。错误产生方法并不需要声明,上游代码也不需要捕获它们,方法的实现得到了有效的隐藏——产生最少的代码混乱。 较新的Java API(比如Spring Framework和Java Data Object库)很少或根本不依赖于已检查异常。Hibernate ORM framework从release 3.0起重新定义了关键设备,以免于使用已检查异常。这反映了由这些框架报告的绝大部分异常异常条件是不可恢复的,这些异常源于方法调用的不正确编码或数据库服务器失效等基本组件原因。实际上,强制调用者去捕捉或声明这样异常几乎没有任何益处。 架构中的错误处理在架构中有效处理错误的第一步是承认处理错误的必要性。承认这一点对于工程师来说有困难,因为他们认为自己有能力创造无缺陷的软件,并引以为豪。下面这些理由可能有所帮助。首先,应用程序开发会在常见错误上花费大量的时间。提供程序员产生的错误将使团队诊断和修复这些错误变得十分简单。第二,对于错误异常条件过度使用Java库中的已检查异常将强制代码来处理这些错误,即使调用顺序完全正确。如果没有适当的错误处理框架,由此产生的暂时异常处理将向应用程序中插入平均信息量。 成功的错误处理框架必须达到四个目标:
错误会分散应用程序的真正目的。因此,用于错误处理的代码数量应当最小化,并在理想情况下,应与程序的语义部分隔离。错误处理必须满足纠错人员的需要。他们需要知道是否发生错误并且获取相关信息以判断错误原因。尽管从定义上说,错误不可恢复,但可靠的错误处理措施将以得体地方式终结出现错误的活动。 对于错误异常条件使用未检查异常有许多原因使我们做出使用未检查异常表示错误异常条件的架构性决定。作为对编程错误的回报,Java运行时将抛出RuntimeException的子类(比如ArithmeticException和ClassCastException),针对架构设定先例。未检查异常使上游方法摆脱了包含无关代码的要求,从而最大限度地减少了混乱。 错误处理策略应当承认Java库和其他API中的方法可能使用已检查异常来表示应用程序环境下的错误异常条件。在这种情况下,采用架构惯例在其出现的地方捕捉API异常,将它作为错误,并抛出未检查异常来说明错误异常条件并捕捉诊断信息。 在这种情况下抛出的特定异常类型应当由架构本身定义。不要忘记错误异常的主要目的是传达诊断信息并记录,以帮助开发人员发现问题产生的原因。使用多错误异常类型可能有些过度,因为架构会对它们进行完全相同的处理。在绝大多数情况下,把良好的描述性文本消息嵌入到单独的错误类型中,便可完成此项工作。使用Java的一般RuntimeException来表示错误条件很容易进行防御。从Java 1.4时起,RuntimeException同其他throwable类一样,支持异常处理链式机制,允许设计人员捕捉并报告导致错误的已检查异常。 设计人员可以定义自己的未检查异常进行报告错误。如果需要使用不支持异常链接机制的Java 1.3或更早版本,这一步是必需的。实现相似的链接功能去捕捉并转换引起应用程序错误的异常相当简单。在错误报告异常中,应用程序可能需要特别的行为。这可能是为架构创建RuntimeException子类的另一个原因。 建立错误屏障决定哪些异常要抛出以及何时抛出将成为错误处理框架的重要决定。同样重要的问题是何时捕捉错误异常及其后如何做。这里的目标是使应用程序的功能部分从处理错误的职责中分离出来。关注点分离通常是比较好的做法。负责处理错误的中央设备将为您带来很多的好处。 在错误屏障(fault barrier)模式下,任何应用程序组件都可以抛出错误异常,但只有作为“错误屏障”的组件才可以捕捉到错误异常。开发人员为了处理错误问题在应用程序中插入了大量复杂代码,而采用此模式可消除大部分此类代码。从逻辑上讲,错误屏障存在于靠近调用堆栈的顶端。在这里,它可以阻断异常向上传播,以避免触发默认动作。默认动作根据应用程序类型的不同而不同。对于独立的Java应用程序来说,默认动作意味着终止活动线程。对于驻留在应用服务器上的Web应用程序,默认动作意味着应用服务器会向浏览器发送不友好的(且令人为难的)响应。 错误屏障组件的首要职责是记录包含在错误异常中的信息,以便进行下一步行动。应用程序日志是迄今为止做这件事情最理想的方法。异常的链信息、堆栈跟踪等对于诊断专家来说都是有价值的信息。发送错误信息最差的地方是通过用户界面。将客户牵涉到应用程序的排错过程中,将对开发人员或客户没有任何益处。如果开发人员真的把诊断信息添加到了用户界面上,这说明开发人员的记录策略需要改进。 错误屏障的下一个职责是以受控方式停止操作。这个职责的含义由应用程序的设计决定,但是通常会涉及到为等待响应的客户端发出总体响应。如果应用程序是Web service,这意味着使用soap:Server的<faultcode>和普通<faultstring>失败消息将SOAP <fault>元素嵌入到响应中。如果应用程序与Web浏览器进行通信,屏障将安排发送普通的HTML响应,表示无法处理此请求。 在Struts应用程序中,错误屏障采用全局异常处理程序的形式,配置成可以处理RuntimeException的任何子类。错误屏障类将扩展org.apache.struts.action.ExceptionHandler,在需要实现自定义处理时重写方法。这将处理由于疏忽产生的错误条件和处理Struts操作中明显发现的错误条件。图2显示了意外事件异常和错误异常。
图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异常模型使用得当,将保持应用程序的简洁性、可维护性和正确性。把面向方面编程技术的错误和意外事件处理作为横切关注点,可为应用程序的架构带来某些明显的优势。 参考资料
|
|||||||||||||||||||
|