如何从未捕获的异常中恢复?

4
如果您想以相同的方式处理每个故障,例如记录并跳过下一个请求、向用户显示消息并处理下一个事件等,则未经检查的异常是可以接受的。如果这是我的用例,我只需要在系统的高层捕获某些通用异常类型,并以相同的方式处理所有内容。
但我想从具体问题中恢复,而且我不确定使用未经检查的异常的最佳方法。以下是一个具体的例子。
假设我有一个 Web 应用程序,使用 Struts2 和 Hibernate 构建。如果异常冒泡到我的“操作”,我会记录它,并向用户显示一个漂亮的道歉。但是,我的 Web 应用程序的一个功能是创建需要唯一用户名的新用户帐户。如果用户选择已存在的名称,则 Hibernate 在我的系统内部抛出 org.hibernate.exception.ConstraintViolationException(未经检查的异常)。我真的很想通过要求用户选择另一个用户名来从这个特定的问题中恢复,而不是给他们相同的“我们记录了您的问题,但现在您被卡住了”的消息。
以下是几点需要考虑的:
1.有很多人同时创建帐户。我不想在“SELECT”查看名称是否存在和“INSERT”之间锁定整个用户表。对于关系数据库,可能有一些技巧可以解决这个问题,但我真正感兴趣的是预先检查异常不起作用的一般情况,因为存在基本竞争条件。同样的事情可能适用于在文件系统上查找文件等。
2.鉴于我的 CTO 喜欢通过阅读“Inc.”中的技术专栏进行管理,我需要在持久性机制周围添加一层间接层,以便我可以丢弃 Hibernate 并使用 Kodo 等而不改变除持久性代码最低层之外的任何内容。实际上,在我的系统中有几个这样的抽象层。如何防止它们泄漏,尽管存在未经检查的异常?
3.已声明的检查异常的一个公开的弱点是必须在堆栈上的每个调用中“处理”它们-通过声明调用方法抛出它们或捕获它们并处理它们。处理它们通常意味着将它们包装在适合于抽象级别的类型的另一个检查异常中。因此,在检查异常领域中,基于文件系统的 UserRegistry 的实现可能会捕获 IOException,而数据库实现则会捕获 SQLException,但两者都将抛出一个隐藏底层实现的 UserNotFoundException。如何利用未经检查的异常,使自己免于在每个层次包装的负担,并避免泄漏实现细节?
9个回答

15

在我看来,将异常(无论是已检查的还是未检查的)包装起来具有几个值得花费代价的好处:

1)它鼓励您思考您编写的代码的故障模式。基本上,您必须考虑调用的代码可能会抛出哪些异常,进而考虑调用您的代码时会抛出哪些异常。

2)它为您提供了向异常链中添加额外调试信息的机会。例如,如果您有一个方法,在重复使用用户名时会引发异常,您可能会使用一个包含关于失败情况的其他信息的异常来包装该异常(例如提供重复用户名的请求的IP)。异常的跟踪记录可能帮助您调试复杂的问题(对我来说肯定有用)。

3)它使您与低级别代码实现独立。如果您正在包装异常并需要将Hibernate更换为其他ORM,则只需更改处理Hibernate的代码即可。所有其他层的代码仍将成功地使用包装的异常,并以相同的方式解释它们,即使底层情况已经发生了变化。请注意,即使Hibernate某种方式发生了变化(例如:他们在新版本中切换异常),这也适用;这不仅适用于批量技术替换。

4)它鼓励您使用不同类别的异常来表示不同的情况。例如,当用户尝试重复使用用户名时,您可能会遇到“DuplicateUsernameException”,而在由于数据库连接中断而无法检查重复用户名时,则可能会遇到“DatabaseFailureException”。这反过来又使您以灵活和强大的方式回答您的问题(“我该如何恢复?”)。如果出现“DuplicateUsernameException”,您可以决定向用户建议一个不同的用户名。如果出现“DatabaseFailureException”,则可以让它上升到显示“维护中”页面的点,并向您发送通知电子邮件。一旦您拥有自定义异常,就有了可定制的响应,这是一件好事。


3
我喜欢在应用程序的“层”之间重新封装异常,例如,DB特定的异常被重新封装在另一个在我的应用程序上下文中有意义的异常中(当然,我将原始异常保留为成员,以免破坏堆栈跟踪)。
话虽如此,我认为非唯一的用户名不是足够“异常”的情况,不值得抛出异常。我会使用布尔型返回参数。如果不了解你的架构,很难说出更具体或适用的内容。

2
请参考《错误生成、处理和管理模式》,了解有关拆分领域和技术错误模式的详细信息。

技术错误不应该导致领域错误的产生(两者不应混淆)。如果技术错误必须导致业务处理失败,则应将其包装为SystemError。

领域错误应始于领域问题,并由领域代码处理。

领域错误应在技术边界上“无缝”传递。可能需要对这些错误进行序列化和重构,以实现这一点。代理和外观应负责执行此操作。

技术错误应在应用程序的特定点处理,例如边界(请参见分布边界处的日志记录)。

随错误返回的上下文信息量取决于这是否对后续诊断和处理(找出替代策略)有用。您需要问自己,来自远程机器的堆栈跟踪是否完全有用于领域错误的处理(尽管错误的代码位置和变量值可能有用)

因此,将Hibernate异常在边界处包装为未经检查的领域异常,例如“UniqueUsernameException”,并让其一直上升到处理程序。即使它不是已检查的异常,也要对抛出的异常进行javadoc!

1
  1. 这个问题与已检查和未检查的辩论并没有真正关系,两种异常类型都适用。

  2. 在 ConstraintViolationException 抛出的地方和我们想要通过显示漂亮的错误消息来处理违规的地方之间,堆栈上有大量的方法调用应该立即中止并且不应该关心问题。这使得异常机制成为正确的选择,而不是从异常到返回值重新设计代码。

  3. 实际上,使用未检查的异常而不是已检查的异常是自然的选择,因为我们确实希望调用堆栈上的所有中间方法都忽略异常并且不处理它。

  4. 如果我们只想通过显示漂亮的错误消息(错误页面)来处理“唯一名称违规”,那么就没有必要使用特定的 DuplicateUsernameException。这将保持异常类的数量较少。相反,我们可以创建一个 MessageException,在许多类似情况下可以重复使用。

    尽快捕获 ConstraintViolationException 并将其转换为带有漂亮消息的 MessageException。当我们可以确定真正违反的是“唯一用户名约束”而不是其他约束时,转换很重要。

    在接近顶层处理程序的某个地方,以不同的方式处理 MessageException。而不是“我们记录了您的问题,但现在您已经无法使用”,只需显示 MessageException 中包含的消息,没有堆栈跟踪。

    MessageException 可以带有一些额外的构造函数参数,例如问题的详细说明、可用的下一步操作(取消、转到其他页面)、图标(错误、警告)...

代码可能长这样

// insert the user
try {
   hibernateSession.save(user);
} catch (ConstraintViolationException e) {
   throw new MessageException("Username " + user.getName() + " already exists. Please choose a different name.");
}

在一个完全不同的地方,有一个顶级异常处理程序。
try {
   ... render the page
} catch (MessageException e) {
   ... render a nice page with the message
} catch (Exception e) {
   ... render "we logged your problem but for now you're hosed" message
}

1

由于您目前正在使用Hibernate,最简单的方法就是检查该异常并将其包装在自定义异常或自定义结果对象中,这些对象可能已经在您的框架中设置好了。如果您以后想要放弃Hibernate,只需确保在一个地方包装此异常,即从Hibernate捕获异常的第一个位置,因为那是您在进行切换时可能需要更改的代码,所以如果catch语句只有一个位置,那么额外的开销几乎为零。

需要帮助吗?


1

我同意Nick的观点。你所描述的异常并不真正意味着“意外异常”,因此在设计代码时应该考虑可能出现的异常情况。

此外,我建议您查看Microsoft Enterprise Library 异常处理块的文档,它有一个很好的错误处理模式概述。


0

您可以捕获未经包装的异常,无需进行包装。例如,以下是有效的 Java 代码。

try {
    throw new IllegalArgumentException();
} catch (Exception e) {
    System.out.println("boom");
}

所以在您的动作/控制器中,您可以将try-catch块放置在进行Hibernate调用的逻辑周围。根据异常,您可以呈现特定的错误消息。

但我猜在你今天可能会使用Hibernate,而明天则是SleepLongerDuringWinter框架。在这种情况下,您需要假装拥有自己的小ORM框架,它包装在第三方框架周围。这将允许您将任何特定于框架的异常包装成更有意义和/或受检查的异常,使您更好地理解。


0

@Jan 检查与未检查是一个核心问题。我对你的假设(#3)提出质疑,即异常应该在中间帧中被忽略。如果我这样做,我的高级代码将会有一个特定于实现的依赖关系。如果我替换Hibernate,整个应用程序中的catch块都必须进行修改。然而,同时,如果我在较低级别捕获异常,那么使用未经检查的异常就没有太多好处。

此外,这里的情况是我想要捕获一个特定的逻辑错误,并通过重新提示用户输入不同的ID来改变应用程序的流程。仅仅更改显示的消息是不够的,而根据异常类型映射到不同消息的能力已经内置于Servlets中。


0

@erikson

只是为了给你的想法增加一些内容:

已检查与未检查也在这里进行了辩论。

未经检查的异常的使用符合它们被用于由函数调用者引起的异常(而调用者可以在该函数的几个层次之上,因此需要其他框架忽略异常)的事实。

关于你的具体问题,你应该在高层捕获未检查的异常,并将其封装,如@Kanook所说,在你自己的异常中,不显示调用堆栈(如@Jan Soltis所述)。

话虽如此,如果底层技术发生变化,那么已经存在于你的代码中的catch()确实会产生影响,这并不能回答你最新的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接