4

如果您想以相同的方式处理每个故障,例如通过记录它并跳到下一个请求,向用户显示消息并处理下一个事件等,未经检查的异常是可以的。如果这是我的用例,我必须做的是在我的系统中捕获一些高级别的一般异常类型,并以相同的方式处理所有事情。

但我想从特定问题中恢复过来,我不确定用未经检查的异常处理它的最佳方法。这是一个具体的例子。

假设我有一个使用 Struts2 和 Hibernate 构建的 Web 应用程序。如果我的“动作”出现异常,我会记录下来,并向用户表示歉意。但是我的 Web 应用程序的功能之一是创建新的用户帐户,这需要一个唯一的用户名。如果用户选择了一个已经存在的名称,Hibernate 会org.hibernate.exception.ConstraintViolationException在我的系统内部抛出一个(未经检查的异常)。我真的很想通过要求用户选择另一个用户名来从这个特定问题中恢复过来,而不是给他们同样的“我们记录了你的问题,但现在你已经被淹没了”的消息。

这里有几点需要考虑:

  1. 有很多人同时创建帐户。我不想在“SELECT”之间锁定整个用户表以查看名称是否存在,如果不存在则“INSERT”。在关系数据库的情况下,可能有一些技巧可以解决这个问题,但我真正感兴趣的是由于基本竞争条件而无法预先检查异常的一般情况。同样的事情也适用于在文件系统上查找文件等。
  2. 考虑到我的 CTO 倾向于通过阅读“Inc.”中的技术专栏来进行偷渡式管理,我需要围绕持久性机制设置一层间接层,这样我就可以抛弃 Hibernate 并使用 Kodo 或其他任何东西,而不需要更改任何东西,除了最低的持久化代码层。事实上,在我的系统中有几个这样的抽象层。尽管有未经检查的异常,如何防止它们泄漏?
  3. 已检查异常的一个公认的弱点是必须在堆栈上的每次调用中“处理”它们——要么通过声明调用方法抛出它们,要么通过捕获它们并处理它们。处理它们通常意味着将它们包装在另一个适合抽象级别的类型的检查异常中。因此,例如,在检查异常领域,我的 UserRegistry 的基于文件系统的实现可能会 catch IOException,而数据库实现会 catch SQLException,但两者都会抛出UserNotFoundException隐藏底层实现的 a。如何利用未经检查的异常,在不泄露实现细节的情况下减轻每一层包装的负担?
4

9 回答 9

15

IMO,包装异常(检查或其他方式)有几个值得付出代价的好处:

1) 它鼓励你思考你编写的代码的失败模式。基本上,你必须考虑你调用的代码可能抛出的异常,然后你会考虑你会为调用你的代码抛出的异常。

2) 它让您有机会将额外的调试信息添加到异常链中。例如,如果您有一个在重复用户名上引发异常的方法,则可以将该异常包装为包含有关失败情况的附加信息(例如,提供重复用户名的请求的 IP)对低级代码不可用。异常的 cookie 跟踪可以帮助您调试一个复杂的问题(它对我来说当然有)。

3)它让你变得独立于底层代码的实现。如果您要包装异常并需要将 Hibernate 换成其他一些 ORM,您只需更改您的 Hibernate 处理代码。所有其他代码层仍将成功使用包装的异常,并以相同的方式解释它们,即使底层环境发生了变化。请注意,即使 Hibernate 以某种方式发生变化,这也适用(例如:它们在新版本中切换异常);这不仅仅是为了批发技术更换。

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

于 2008-08-28T22:32:53.390 回答
3

我喜欢在我的应用程序的“层”之间重新打包异常,例如,一个特定于 DB 的异常被重新打包在另一个异常中,这在我的应用程序的上下文中是有意义的(当然,我将原始异常保留为成员所以我不会破坏堆栈跟踪)。

也就是说,我认为一个非唯一的用户名并不是一个足够“例外”的情况来保证抛出。我会改用布尔返回参数。在不了解您的架构的情况下,我很难说出更具体或更适用的内容。

于 2008-08-28T22:02:57.940 回答
2

请参阅错误的生成、处理和管理模式

从拆分域和技术错误模式

技术错误绝不应导致生成域错误(两者不应相遇)。当技术错误必须导致业务处理失败时,应将其包装为 SystemError。

域错误应始终从域问题开始,并由域代码处理。

域错误应该通过技术边界“无缝”传递。可能必须对此类错误进行序列化和重构才能发生这种情况。代理和外墙应该为此负责。

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

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

因此,在边界处包装休眠异常以使用未经检查的域异常(例如“UniqueUsernameException”)休眠,并让它一直冒泡到它的处理程序。确保对抛出的异常进行 javadoc,即使它不是已检查的异常!

于 2008-09-16T18:09:39.387 回答
1

由于您当前使用的是休眠,因此最简单的做法就是检查该异常并将其包装在自定义异常或自定义结果对象中,您可能在框架中进行了设置。如果您想稍后放弃休眠,只需确保您仅将这个异常包装在一个地方,即您从休眠中捕获异常的第一个地方,这就是您在进行切换时可能必须更改的代码,所以如果捕获在一个地方,那么额外的开销几乎是 zilch。

帮助?

于 2008-08-28T22:05:52.130 回答
1

我同意尼克的观点。您描述的异常并不是真正的“意外异常”,因此您应该相应地设计代码,考虑可能的异常。

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

于 2008-08-28T22:19:56.480 回答
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
}
于 2008-09-02T15:16:34.447 回答
0

您可以捕获未经检查的异常,而无需包装它们。例如,以下是有效的 Java。

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

因此,在您的操作/控制器中,您可以在进行 Hibernate 调用的逻辑周围有一个 try-catch 块。根据异常,您可以呈现特定的错误消息。

但我想在你今天它可能是 Hibernate,明天是 SleepLo​​ngerDuringWinter 框架。在这种情况下,您需要假装拥有自己的小型 ORM 框架,该框架围绕着第三方框架。这将允许您将任何特定于框架的异常包装成您知道如何更好地理解的更有意义和/或检查的异常。

于 2008-08-28T23:36:12.693 回答
0

@Jan Checked 与 unchecked 是这里的核心问题。我质疑您的假设(#3),即在中间帧中应该忽略异常。如果我这样做,我最终会在我的高级代码中产生特定于实现的依赖关系。如果我替换 Hibernate,则必须修改整个应用程序中的 catch 块。然而,与此同时,如果我在较低级别捕获异常,我不会从使用未经检查的异常中获得太多好处。

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

于 2008-09-02T20:56:00.823 回答
0

@erikson

只是为了给你的想法添加食物:

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

未经检查的异常的使用符合 IMO 用于由函数调用者引起的异常的事实(调用者可以在该函数之上几层,因此其他框架必须忽略异常)

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

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

于 2008-09-16T15:30:12.453 回答