Effective Java 3汪汪队中文版版

Java开发人员可以做出的最重要的架構决策之一是如何使用Java异常模型Java异常一直是社区争论的主题。 有些人认为Java语言中的checked(受检)异常是一个失败的实验 本文认为,错误不在于Java模型而在于Java库设计者未能认知到方法失败的两个基本原因。 本文提倡一种思考异常情形性质的方法并描述有助于您设计的设计模式。 朂后本文讨论了异常处理作为面向切面编程模型中的横切关注点。 正确使用Java异常是一个很大的好处 本文将帮助您做到这一点。

Java应用程序中的异常处理可以告诉您很多用于构建它的体系结构的强大 架构是关于在应用程序的所有级别上一致地做出和遵循的决定。 要做出的朂重要的决定之一是应用程序中的类子系统或应用内的各个层的相互通信的方式。 Java异常是方法传递操作的替代结果的方式因此在你的應用体系结构中值得特别关注。

衡量Java架构师技能和开发团队纪律的一个好方法是查看其应用程序中的异常处理代码 首先要注意的是,有哆少代码用于捕获异常记录异常,尝试确定发生了什么情况以及将一个异常转换为另一个异常 清晰,紧凑和一致的(coherent)异常处理表明团队具有一致的使用Java异常的方法 当异常处理代码的数量可能超过其他所有代码时,您可以判断团队成员之间的交流已经崩溃(或者从一开始僦不存在)并且每个人都以“他们自己的方式”处理异常。

临时异常处理的结果是非常容易预测的 如果你问团队成员他们为什么在他們的代码中的特定地方抛出、捕获或忽略异常,回答通常是“我不知道还能做什么” 如果你问他们如果他们代码中的异常确实发生了会怎样,那么他们就会皱眉头了你会得到一个类似于“我不知道。我们从未测试过它”的回答

你可以通过查看Java组件的客户端(client)代码来判断咜是否有效地使用了Java异常。 如果它们包含大量逻辑以确定操作何时失败为什么失败,原因几乎总是因为组件的错误报告设计(error reporting design)有问题 有缺陷的报告在客户端产生大量“记录和遗忘”(log and forget)代码,很少有用 最糟糕的是扭曲的逻辑路径,嵌套的try/catch/finally块以及其它导致应用程序脆弱且难鉯管理的混乱。

将异常作为事后补救(或根本不解决它们)是导致软件项目混乱和延期的主要原因 异常处理是一个涉及设计的所有部分嘚问题。 建立异常的架构约定应该是项目需要做的第一个决策 正确地使用Java异常模型将大大有助于保持应用程序的简单性,可维护性和正確性

正确使用Java异常模型需要怎么做一直是争论的主题。 Java不是第一种支持异常语义的编程语言; 但是它是编译器强制执行规则以管理某些異常的声明和处理的第一种语言。 许多人认为编译时异常检查有助于精确的软件设计与其他语言特性很好地协调工作。 图1显示了Java异常层佽结构

通常,Java编译器会强制一个抛出基于java.lang.Throwable的异常的方法在方法声明中有“throws”子句。 此外编译器验证方法的客户端是捕获声明的异常類型还是指定它们自己抛出该异常类型。 这些简单的规则对全世界的Java开发人员产生了深远的影响

编译器将Throwable继承树的两个分支的异常检查荇为分开处理。 java.lang.Errorjava.lang.RuntimeException的子类编译时不用检查 在这两者中,软件设计者通常对运行时异常更感兴趣 术语“未经检查”(unchecked)的异常即指运行时异瑺,它们区别于所有其它“已检查”(checked)的异常

我想,那些同样重视Java强类型的人也接受了checked异常毕竟,编译器对数据类型施加的约束是对严格的编码和精确的思考的一种鼓励编译时类型检查有助于防止在运行时出现令人讨厌的意外情况。编译时异常检查的工作方式类似提醒开发人员方法具有需要解决的潜在的其他结果。

在早期建议尽可能使用已检查的异常,以最大限度地利用编译器提供的帮助来生产无錯误的软件 Java库的API设计者显然订阅了已检查的异常标准,使用这些异常来广泛地模拟库方法中可能发生的任何意外事件在J2SE 5.1 API规范中,已检查的异常类型仍然超过未经检查的类型的两倍以上

对于程序员来说,似乎Java库类中的大多数常用方法都会为每个可能的失败声明checked异常例洳,java.io包严重依赖于checked异常IOException至少有63个Java库包直接或通过其中的几个子类之一抛出此异常。

I/O失败是一个严重但非常罕见的事件最重要的是,您嘚代码通常无法从一个IO失败中恢复 Java程序员发现自己在简单的Java库方法调用中被迫提供IOException和类似的不可恢复的事件。捕获这些异常会给原本简單的代码增加混乱因为在catch块中几乎做不了什么事情来帮助解决这个问题。没有捕获它们可能更糟因为编译器要求你将它们添加到方法拋出的异常列表中。这暴露了良好的面向对象设计自然的应该隐藏的实现细节

这种没有赢家的局面导致了大多数现在我们警告的臭名昭著的反模式异常处理。它还以正确的和错误的方法提出了许多建议来构建变通方法

一些Java名人开始质疑Java的checked异常模型是否是一个失败的实验。有些事情确实失败了但它与在Java语言中包含异常检查无关。失败的原因在于Java API设计者认为大多数失败情况都是相同的并且可以通过相同嘚异常进行传达。

考虑一个虚构的银行应用程序中的CheckingAccount类 CheckingAccount属于客户,维护当前余额并且能够接受存款,接受支票上的止付订单以及处理收到的支票 CheckingAccount对象必须协调并发线程的访问,其中任何一个线程都可能改变其状态CheckingAccount的processCheck()方法接受Check对象作为参数,通常从帐户余额中扣除支票金额但是调用processCheck()的检查清除客户端必须准备好应对两个意外事件。首先CheckingAccount可能有一个为支票注册的止付订单。其次账户可能没有足够嘚资金来支付支票金额。

因此processCheck()方法可以以三种可能的方式响应其调用者。正常的响应是检查得到处理方法签名中声明的结果返回给调鼡服务。这两个意外响应代表了非常真实的银行领域中需要传达给支票清算客户的情况所有三个processCheck()响应都是有意设计的,用于模拟典型支票账户的行为

在Java中表示意外响应的自然方式是定义两个异常,比如StopPaymentException和InsufficientFundsException客户端忽略这些是不对的,因为它们肯定会被抛入应用程序的正瑺操作中它们有助于表达方法的完整行为,与方法签名一样重要

客户端可以轻松处理这两种异常。如果支票上的付款被停止客户端鈳以将支票路由到特殊处理的逻辑。如果资金不足客户端可以从客户的储蓄账户转移资金以支付支票,然后再试一次

意外事件以及使鼡CheckingAccount API的正常流程都被预测了。它们不代表软件或运行环境的故障将这些与由于与CheckingAccount类的内部实现细节相关的问题而可能出现的真正的失败进荇对比。

想象一下CheckingAccount在数据库中维护其持久状态,并使用JDBC API来访问它由于与CheckingAccount的实现无关的原因,该API中几乎每种数据库访问方法都有可能失敗例如,有人可能忘记打开数据库服务器拔掉网络电缆或更改访问数据库所需的密码。

JDBC依赖于单个checked异常SQLException来报告可能出错的所有内容夶多数可能出错的地方都与配置数据库,连接数据库及其所在的硬件有关processCheck()方法无法以有意义的方式处理这些情况。processCheck()至少知道它自己的实現调用堆栈中的上游方法具有更小的解决问题的可能性。

CheckingAccount示例说明了方法执行无法返回其预期结果的两个基本原因它们值得一些描述性术语:

方法可以用抛出异常的方式表达可能的异常(方法罕见的结果,但是是方法可能的返回值只是不太常见,比如账户余额不足异常) 该方法的调用者具有应对它们的策略。

一种出乎意料的情况使得方法不能返回预期的值,如果不参考方法的内部实现则无法对其进荇描述。

使用此术语停止付款订单和透支是processCheck()方法的两种可能的意外(Contingency)情况。 SQL问题表示可能的故障(Fault)情况 processCheck()的调用者应该有一种方法来处理意外(Contigency)事件,但如果发生故障(Fault)这种情况则无法合理地预期处理它。

在意外和故障方面考虑“可能出现的问题”将大大有助于在应用程序架构Φ建立Java异常约定

程序bug、硬件故障、配置错误、缺少文件、服务器访问不了

意外情况很好地映射到Java的checked异常。由于它们是方法语义契约的组荿部分因此有必要使用编译器的帮助来确保它们得到解决。如果你发现编译器强制你在不方便的情况下处理或声明异常那么你的设计需要重构一下了。这实际上是件好事

人们对故障情况(Fault conditions)很感兴趣,但是软件逻辑却不然那些扮演“软件直肠病学家”角色的人需要有关故障的信息来诊断和修复导致它们发生的任何事情。因此未经检查的Java异常是表示故障的完美方式。它们允许故障通知通过调用堆栈上的所有方法不受影响地渗透到专门设计用于捕获它们的层面(level)捕获它们包含的诊断信息,并为程序活动提供受控且优雅的结论能够产生故障的方法不需要在方法上声明它,不需要上游方法来捕获它们并且方法的实现保持适当隐藏 - 所有这些都具有最少的代码混乱。

ORM框架从3.0版開始重新定义了主要部分以消除checked异常的使用。这反映了这样的认识即这些框架所报告的绝大多数异常情况都是不可恢复的,这些异常凊况源于方法调用的错误编码或某些底层组件(如数据库服务器)的故障实际上,通过强制调用者捕获或声明此类异常几乎没有任何好處

在您的架构中有效处理故障的第一步是承认你需要这样做。 对于那些以创造无可挑剔的软件能力而自豪的工程师而言很难接受这种認可。 下面是一些有帮助的理由 首先,你的应用程序将花费大量时间进行开发其中错误是司空见惯的。 提供程序员造成的故障将使你嘚团队更容易诊断和修复它们 其次,在Java库中(过度)使用checked异常来处理故障情况将迫使你的代码处理它们即使你的调用顺序完全正确。 洳果没有适当的故障处理框架临时异常处理会将熵注入你的应用程序。

一个成功的错误处理框架需要完成四个目标:

  • 捕获错误并且保存診断信息

错误会分散应用程序的真实目的 因此,用于处理它们的代码量应该是最小的并且理想地,与应用程序的语义部分隔离 故障處理必须满足负责纠正它们的人的需要。 他们需要知道发生了故障并获得有助于他们弄清楚原因的信息。 即使根据定义故障不可恢复,良好的故障处理也会尝试以优雅的方式终止遇到故障的活动

有许多理由使架构决策用unchecked异常来表示故障情况。 Java运行时通过抛出RuntimeException子类(如ArithmeticException囷ClassCastException)来"奖励"编程错误为你的体系结构设置先例。unchecked异常通过让上游方法不用处理和它意图无关的错误而使得代码更整洁

你的故障处理策畧应该认识到Java库和其他API中的方法可能使用checked异常来表示应用程序上下文中的故障条件。在这种情况下采用体系结构约定来捕获发生的API异常,将其视为故障并抛出unchecked异常以发出故障信号并捕获诊断信息。

在这种情况下抛出的特定异常类型应该由你的体系结构定义不要忘记,故障异常的主要目的是传达将被记录的诊断信息以帮助人们找出问题所在。使用多个故障异常类型可能有点过分因为你的体系结构将唍全相同地处理它们。嵌入在单个故障异常类型中的良好描述性消息将在大多数情况下完成工作使用Java的通用RuntimeException来表示你的故障情况很容易。从Java

你可以选择为故障报告定义自己的unchecked异常如果你需要使用不支持异常链的Java 1.3或更早版本,则必须执行此操作实现类似的链功能可以很嫆易地捕获和转换构成应用程序故障的checked异常。你的应用程序可能需要在故障报告异常中执行特殊操作这将是为你的体系结构创建RuntimeException子类的叧一个原因。

决定抛出哪个异常以及何时抛出它是你的故障处理框架的重要决策关于何时捕获故障异常以及之后要做什么的问题同样重偠。这里的目标是使应用程序的功能部分免于处理故障的责任关注点分离通常是件好事,负责处理故障的中央设施将在未来发挥作用

茬故障屏障模式中,任何应用程序组件都可以引发故障异常但只有充当“故障屏障”的组件才能捕获它们。采用这种模式消除了开发人員在本地插入处理故障的大量复杂代码故障屏障逻辑上位于调用堆栈的顶部,在触发默认操作之前它停止向上传播异常默认操作根据應用程序类型的不同而不同。对于独立Java应用程序这意味着活动线程终止。对于由应用程序服务器托管的Web应用程序这意味着应用程序服務器向浏览器发送不友好(且令人尴尬)的响应。

故障屏障组件的第一个职责是记录故障异常中包含的信息以便将来采取措施。到目前為止应用程序日志是执行此操作的最佳位置。异常的链接消息堆栈跟踪等对于诊断人员来说都是有价值的信息。发送故障信息的最差位置是跨用户界面让你的应用程序的客户端参与调试过程对你或你的客户来说几乎没有任何好处。如果你真的想要用诊断信息绘制用户堺面则可能意味着你的日志记录策略需要改进。

故障屏障的下一个责任是以受控方式关闭操作这意味着什么取决于你的应用程序设计,但通常涉及生成对可能正在等待的客户端的通用响应如果应用程序是Web服务,则意味着使用soap:Server的<faultcode>和通用<faultstring>失败消息在响应中构建SOAP <fault>元素如果应用程序与Web浏览器通信,屏障将安排发送通用HTML响应指示无法处理请求。

在Struts应用程序中你的故障屏障可以采用全局异常处理器的形式,该处理器被配置为处理RuntimeException的任何子类你的故障屏障类将扩展org.apache.struts.action.ExceptionHandler,根据需要覆盖方法以实现你需要的自定义处理这将处理在处理Struts操作期间奣显发现的无意生成的故障条件和故障情况。图2显示了意外事件和故障异常

如果您正在使用Spring MVC框架,则可以通过扩展SimpleMappingExceptionResolver并将其配置为处理RuntimeException及其子类来轻松构建故障屏障 通过重写resolveException()方法,你可以在使用超类方法将请求路由到发送通用错误显示的视图之前添加所需的任何自定义处悝

当你的架构包含故障屏障并且开发人员意识到它时,编写一次性故障异常处理代码的诱惑就会大大减少 结果是整个应用程序中的代碼更清晰,更易于维护

随着故障处理降级到屏障,主要组件之间的应急通信变得更加简单意外事件代表一种替代方法结果,与主要的返回结果同样重要因此,checked异常类型是传达意外情况存在的良好工具并提供处理意外所需的信息。这种做法需要Java编译器的帮助以提醒開发人员他们正在使用的API的所有方面以及需要提供所有方法结果。

通过单独使用方法的返回类型可以传达简单的意外事件例如,返回空引用而不是实际对象可以表示因为某种原因无法创建对象 Java I/O方法通常返回整数值-1,而不是字节值或字节数以表示文件结束的情况。如果伱的方法的语义足够简单那么替代返回值可能是最好的选择,因为它们消除了异常带来的开销缺点是方法调用者负责测试返回值以查看它是主要结果还是偶然结果。但是编译器不会强制方法调用者进行该测试。

如果方法具有void返回类型则异常是表示发生意外事件的唯┅方法。如果方法返回对象引用则返回值可以表达的词汇表限制为两个值(null和non-null)。如果方法返回一个整数值则可以通过选择保证不与主要返回值冲突的值来表达几个意外情况。但现在我们已经进入了错误代码检查的世界开发了Java异常模型以避免这种情况。

定义不同的故障(fault)报告异常类型没有多大意义因为故障屏障对它们的处理方式完全相同。 意外(contingency)异常是完全不同的因为它们旨在向方法调用者传达不同嘚条件,你的体系结构可能会指定这些异常(指的是contingency exception)应该扩展java.lang.Exception或指定的基类

不要忘记你的异常是完整的Java类型,它们可以包含专门的字段方法,甚至可以为你的独特目的而设计的构造函数 例如,假想的CheckingAccount processCheck()方法抛出的InsufficientFundsException类型可以包含一个OverdraftProtection对象该对象能够帮助转入所需的资金以彌补账户余额的不足。

记录故障(fault)异常是有意义的因为它们的目的是引起人们注意需要纠正的情况。 对于意外(contingency)异常则不能这么说,因为咜们可能代表相对罕见的事件但预计它们中的每一个都会在你的应用程序生命周期中发生。 如果有的话它们仅仅表示应用程序的运行方式与其工作方式相同罢了。 将记录代码添加到意外(contingency)捕获块会增加代码的混乱而没有实际的好处。 如果意外事件代表重大事件则在抛絀意外事件异常以警告其调用者之前,生成记录事件的日志的方法可能更好

在面向切面(也叫方面)编程(AOP)术语中,故障(fault)和意外(contingency)处理是横切关注的问题 例如,要实现故障屏障模式所有参与的类必须遵循常见的约定:

  • 故障屏障方法必须位于遍历参与类的方法调用图的头部。
  • 它们都必须使用unchecked异常来表示故障(fault)情况
  • 它们必须全部使用故障屏障期望接收的特定unchecked异常类型。
  • 它们都必须从被认为是执行上下文中的错誤发生的较低层方法中捕获并转换checked异常
  • 它们不得干扰故障异常在通往屏障的途中传播。

这些担忧跨越了其他无关类的界限结果是一小蔀分分散的故障处理代码和屏障类与参与者之间的隐式耦合(尽管仍然比完全不使用模式有很大改进!)。AOP允许将故障处理问题封装在应鼡于参与类的公共Aspect中诸如AspectJ和Spring AOP之类的Java AOP框架将异常处理识别为可以附加故障处理行为(或建议)的连接点。以这种方式可以放宽绑定故障屏障模式中的参与者的约定。故障处理现在可以驻留在独立的外部方面从而无需将“屏障”方法放置在方法调用序列的头部

如果你在架构中使用AOP则故障(fault)和意外(contingency)处理是适用于整个应用程序的方面的理想候选者。全面探讨故障和意外处理如何在AOP世界中发挥作用将成为未来攵章的一个有趣话题

虽然Java异常模型在其生命周期中产生了激烈的讨论,但它在被正确应用时提供了极大的价值 作为架构师,你需要建竝从模型中获得最大收益的约定 在故障和意外事件方面考虑例外可以帮助你做出正确的选择。 正确使用Java异常模型将使你的应用程序简单可维护和正确。 面向切面编程技术可以通过将故障和意外处理识别为横切关注点来为你的架构提供一些明确的优势

}

我要回帖

更多关于 中文版 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信