摘要

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设计者认为大多数失败情况都是相同的,并且可以通过相同的异常进行传达。

故障和意外(Faults and Contingencies)

考虑一个虚构的银行应用程序中的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示例说明了方法执行无法返回其预期结果的两个基本原因。它们值得一些描述性术语:

意外(Contingency)

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

故障(Fault)

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

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

匹配Java异常

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

情况(Condition) 意外(Contingency) 故障(Fault)
被认为是 设计中的一部分 令人厌恶的"惊喜"
是否被期望发生 希望很少发生 希望永不发生
谁关注它 调用方法的上游方法 需要处理问题的人
例子 替代返回模式(Alternative return modes) 程序bug、硬件故障、配置错误、缺少文件、服务器访问不了
最佳匹配 checked异常 unchecked异常

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

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

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

在你的架构中处理故障

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

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

  • 最小化代码混乱
  • 捕获错误并且保存诊断信息
  • 提醒正确的人
  • 优雅地退出程序

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

为故障情况使用unchecked异常

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

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

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

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

建立一个故障屏障(Establish a fault barrier)

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

在故障屏障模式中,任何应用程序组件都可以引发故障异常,但只有充当“故障屏障”的组件才能捕获它们。采用这种模式消除了开发人员在本地插入处理故障的大量复杂代码。故障屏障逻辑上位于调用堆栈的顶部,在触发默认操作之前它停止向上传播异常。默认操作根据应用程序类型的不同而不同。对于独立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()方法,你可以在使用超类方法将请求路由到发送通用错误显示的视图之前添加所需的任何自定义处理。

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

你架构中的意外处理(Contingency Handling in Your Architecture)

随着故障处理降级到屏障,主要组件之间的应急通信变得更加简单。意外事件代表一种替代方法结果,与主要的返回结果同样重要。因此,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异常模型将使你的应用程序简单,可维护和正确。 面向切面编程技术可以通过将故障和意外处理识别为横切关注点来为你的架构提供一些明确的优势。

原文链接:https://www.oracle.com/technetwork/java/effective-exceptions-092345.html

高效的java异常(Effective Java Exceptions)相关推荐

  1. Java异常(一) Java异常简介及其架构

    概要 本章对Java中的异常进行介绍.内容包括: Java异常简介 Java异常框架 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3544168.htm ...

  2. JAVA异常:java.lang.AbstractMethodError: ...tomcat.websocket.server.WsSessionListener.sessionCreated

    JAVA异常:java.lang.AbstractMethodError: org.apache.tomcat.websocket.server.WsSessionListener.sessionCr ...

  3. Java 异常(Java Exception)(一)

    Java异常 异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 过API中Throwable类的众多子类描述各 ...

  4. java异常库,java中的异常详解

    java中的exception关系图如图下图所示: Throwable 是Exception(异常)和Error(错误)的超类!! 两者的区别: Exception表示程序需要捕捉和处理的的异常; E ...

  5. java异常网,Java异常实践事项

    在大学项目开发中, 你有没发现自己做的项目总是出现bug,不仅仅出现bug,而且很难根据异常信息找到异常源.我当时也是非常懊恼, 可怕的是不知道怎么维护... 软件Java异常需要理解基础的知识, 在 ...

  6. Java:Effective java学习笔记之 考虑实现Comparable 接口

    Java 考虑实现Comparable 接口 考虑实现Comparable 接口 1.Comparable接口 2.为什么要考虑实现Comparable接口 3.compareTo 方法的通用约定 4 ...

  7. java.net.malf,得到java异常:java.net.MalformedURLException:没有协议

    I am currently calling the following line of code: java.net.URL connection_url = new java.net.URL(&q ...

  8. Java四大名著--effective java

    osc动弹上的弹友推荐的,还是很不错的,有时间打算看看其它的几大"名著". 最近看了看Java程序设计语言,一览而过,是一本很适合初学者的书,嗯...拿来复习也是很不错的. 然后就 ...

  9. java异常对象引用变量_Java面向对象编程-异常处理

    第九章 异常处理 异常情况会改变正常的流程,导致恶劣的后果,为了减少损失,应该事先充分预料所有可能出现的异常,然后采取以下措施: 首先考虑避免异常,彻底杜绝异常的发生:如果不能完全避免,则尽可能地减少 ...

最新文章

  1. 专业的java培训机构是否靠谱,对比一下就知道了!
  2. POSIX标准总体分析
  3. 中国移动雄安研究院 2020校园招聘笔试JAVA方向(一)
  4. linux命令:vmstat
  5. 我是状态机,有一颗永远骚动的机器引擎
  6. java 邮件模板_Spring Boot 优雅地发送邮件
  7. C#二维数组的定义和初始化
  8. 嵌入式常见笔试题总结(2)
  9. c语言程序设计课程设计心得体会,C语言程序课程设计心得体会
  10. 联想g510升级方案_联想智慧中国行,聚焦第一古城,助力企业智能升级
  11. 孙高飞:人工智能测试_高飞学习钓鱼:为什么好的文档很重要
  12. Python-OpenCV人脸检测(代码)
  13. 用PHP语言做网站常见漏洞有哪些?
  14. JDK 1.6 API 中文版
  15. 基于ssm医院病历管理系统
  16. Sql Server 2005 64位安装包
  17. .NET源码 生产制造业通用管理ERP系统 财务生产管理网站 源码
  18. Hikari 数据库连接池配置详解
  19. 软件架构风格整理(1 数据流风格)
  20. JS实现将数字金额转换为大写人民币汉字的方法

热门文章

  1. 2019-12-20
  2. Linux-2.6驱动开发 附录一 设备名称
  3. message from server: “Host ‘DESKTOP-FAJUM7V‘ is not allowed to connect to this MySQL server“
  4. android高德地图用地址获取经纬度,高德地图API-获取位置信息的经纬度
  5. 两分钟了解数据封装和解封
  6. Cadence Allegro自动放置所有元件图文教程及视频演示
  7. windows10 飞秋不能发送文件 防火墙设置
  8. 玲珑杯-射击气球-点到线段的距离
  9. http请求头中的host是什么意思
  10. 【高级篇 / FortiGate-VM】(6.4) ❀ 04. 虚拟 PC 通过 FortiGate VM 上网 ❀ FortiGate 防火墙