如何从未经检查的异常中恢复?

时间:2020-03-05 18:44:43  来源:igfitidea点击:

如果我们希望以相同的方式处理每个失败,则可以使用未检查的异常,例如,通过记录故障并跳至下一个请求,向用户显示消息并处理下一个事件,等等。如果这是我的用例,那么我要做的就是在我的系统中高层捕获一些常规的异常类型,并以相同的方式处理所有事情。

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

假设我有一个使用Struts2和Hibernate构建的Web应用程序。如果异常冒充了我的"操作",我将其记录下来,并向用户表示歉意。但是我的Web应用程序的功能之一是创建新的用户帐户,该帐户需要唯一的用户名。如果用户选择一个已经存在的名称,则Hibernate会在我的系统中抛出一个org.hibernate.exception.ConstraintViolationException(未经检查的异常)。我真的很想从特定的问题中恢复过来,方法是要求用户选择另一个用户名,而不是给他们相同的"我们记录了问题,但现在我们很忙"的消息。

这里有几点要考虑:

  • 有很多人同时创建帐户。我不想将整个用户表锁定在"选择"(SELECT)以查看名称是否存在,如果不存在,则锁定" INSERT"。在关系数据库的情况下,可能有一些技巧可以解决此问题,但是我真正感兴趣的是在一般情况下,由于基本的竞争条件而无法进行异常的预检查。同样的事情可能适用于在文件系统上查找文件等。
  • 鉴于我的CTO因阅读" Inc."中的技术专栏而引起的驾车管理倾向,因此我需要在持久性机制周围进行一层间接操作,以便我可以抛弃Hibernate并使用Kodo或者其他任何东西,而除了最低要求外,无需进行任何更改持久性代码层。实际上,我的系统中有几个这样的抽象层。尽管有未经检查的异常,如何防止它们泄漏?
  • 声明的检查异常的弱点之一是必须通过声明调用方法将其抛出,或者通过捕获并处理它们来"处理"堆栈中的每个调用。处理它们通常意味着将它们包装在另一个适合抽象级别的已检查异常中。因此,例如,在检查例外区域中,我的UserRegistry的基于文件系统的实现可能会捕获到" IOException",而数据库实现会捕获到" SQLException",但是两者都将抛出" UserNotFoundException",从而隐藏了底层实现。我如何利用未检查的异常,从而避免在每一层进行这种包装的负担,而又不泄漏实现细节?

解决方案

回答

我喜欢在应用程序的"层"之间重新包装异常,例如,将特定于数据库的异常重新包装在另一个对我的应用程序有意义的异常内部(当然,我将原始异常保留为成员,所以我不会破坏堆栈跟踪)。

就是说,我认为非唯一的用户名不足以保证抛出异常。我会使用布尔返回参数来代替。在不了解体系结构的情况下,我很难说出更具体或者更适用的内容。

回答

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

帮助?

回答

我同意尼克。我们描述的异常并不是真正的"意外异常",因此我们应该在设计代码时考虑到可能的异常。

另外,我建议我们查看一下Microsoft Enterprise Library Exception Handling Block的文档,该文档对错误处理模式进行了很好的概述。

回答

IMO包裹异常(检查或者其他方式)有几个好处,这些都是值得的:

1)鼓励我们考虑编写代码的失败模式。基本上,我们必须考虑调用的代码可能引发的异常,然后依次考虑为调用代码引发的异常。

2)它使我们有机会将其他调试信息添加到异常链中。例如,如果我们有一种方法会在重复的用户名上引发异常,则可以用包含有关失败情况的其他信息(例如,提供重复用户名的请求的IP)的异常包装该异常不适用于较低级别的代码。 Cookie异常跟踪可能会调试一个复杂的问题(对我来说当然有)。

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

4)鼓励我们使用不同类别的异常来表示不同情况。例如,当用户尝试重用用户名时,我们可能会遇到DuplicateUsernameException;当由于数据库连接断开而无法检查重复的用户名时,可能会出现DatabaseFailureException。这样一来,我们就可以灵活而有效地回答自己的问题("如何恢复?")。如果收到DuplicateUsernameException,则可以决定建议该用户使用其他用户名。如果收到DatabaseFailureException,则可以让它冒泡,直到它向用户显示"停机维护"页面,并向我们发送通知电子邮件。一旦有了自定义的异常,便有了可自定义的响应-这是一件好事。

回答

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

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

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

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

回答

  • 这个问题与检查还是非检查辩论并没有真正的关系,这两种情况都适用。
  • 在抛出ConstraintViolationException的点与我们要通过显示一条不错的错误消息来处理违例的点之间,堆栈上有大量方法调用,这些方法调用应立即中止并且不关心问题。与将代码从异常重新设计为返回值相反,这使得异常机制是正确的选择。
  • 实际上,使用非检查异常而不是检查异常是很自然的做法,因为我们确实希望调用堆栈上的所有中间方法都忽略该异常而不对其进行处理。
  • 如果我们只想通过向用户显示错误消息(错误页面)来处理"唯一名称冲突",则实际上并不需要特定的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
}

回答

@Jan选中与未选中是这里的中心问题。我对假设(#3)表示怀疑,在中间帧中应忽略该异常。如果这样做,我将在高级代码中得到特定于实现的依赖。如果我替换了Hibernate,则必须修改整个应用程序中的catch块。但是,与此同时,如果我在较低的级别上捕获到该异常,则使用未检查的异常不会给我带来太大的好处。

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

回答

@埃里克森

只是为了增加想法:

在这里也检查了检查还是未检查

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

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

话虽这么说,如果底层技术发生变化,那确实会对代码中已经存在的catch()产生影响,并且无法解决最新情况。

回答

请参见生成,处理和处理模式
错误管理

来自"拆分域和技术错误"模式

A technical error should never cause a
  domain error to be generated (never
  the twain should meet). When a
  technical error must cause business
  processing to fail, it should be
  wrapped as a SystemError. 
  
  Domain errors should always start from a
  domain problem and be handled by
  domain code. 
  
  Domain errors should
  pass "seamlessly" through technical
  boundaries. It may be that such errors
  must be serialized and re-constituted
  for this to happen. Proxies and
  facades should take responsibility for
  doing this. 
  
  Technical errors should
  be handled in particular points in the
  application, such as boundaries (see
  Log at Distribution Boundary). 
  
  The
  amount of context information passed
  back with the error will depend on how
  useful this will be for subsequent
  diagnosis and handling (figuring out
  an alternative strategy). You need to
  question whether the stack trace from
  a remote machine is wholly useful to
  the processing of a domain error
  (although the code location of the
  error and variable values at that time
  may be useful)

因此,将休眠异常包装在边界以使用未经检查的域异常(例如" UniqueUsernameException")进行休眠,并使其一直冒泡至其处理程序。即使不是经过检查的异常,也请务必使用javadoc引发异常!