【问题标题】:How do I recover from an unchecked exception?如何从未经检查的异常中恢复?
【发布时间】:2010-09-07 04:54:03
【问题描述】:

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

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

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

这里有几点需要考虑:

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

【问题讨论】:

    标签: c# java api exception


    【解决方案1】:

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

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

    【讨论】:

      【解决方案2】:

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

      帮助?

      【讨论】:

        【解决方案3】:

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

        我还建议您查看 Microsoft Enterprise Library Exception Handling Block 的文档,它有一个很好的错误处理模式大纲。

        【讨论】:

          【解决方案4】:

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

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

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

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

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

          【讨论】:

            【解决方案5】:

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

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

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

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

            【讨论】:

              【解决方案6】:
              1. 这个问题与已检查与未检查的辩论没有真正的关系,这同样适用于两种异常类型。

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

              3. 1234563处理它。
              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
              }
              

              【讨论】:

                【解决方案7】:

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

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

                【讨论】:

                  【解决方案8】:

                  @erikson

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

                  选中与未选中也是debated here

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

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

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

                  【讨论】:

                    【解决方案9】:

                    Patterns for Generation, Handling and Management of Errors

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

                    技术错误绝不应该导致 要生成的域错误(从不 两人应该会面)。当一个 技术错误必然导致业务 处理失败,应该是 包装为 SystemError。

                    域错误应始终从 域问题并由 域代码。

                    域错误应该 通过技术“无缝”传递 边界。可能是这样的错误 必须序列化和重构 发生这种情况。代理和 外墙应该负责 这样做。

                    技术错误应该 在特定点进行处理 应用程序,如边界(见 在分布边界记录)。

                    传递的上下文信息量 返回错误将取决于如何 有用的,这将用于后续 诊断和处理(弄清楚 另一种策略)。你需要 质疑堆栈跟踪是否来自 远程机器对 域错误的处理 (虽然代码位置 当时的错误和变量值 可能有用)

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

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2010-11-08
                      • 1970-01-01
                      • 1970-01-01
                      • 2019-03-30
                      • 1970-01-01
                      • 2010-10-03
                      • 1970-01-01
                      相关资源
                      最近更新 更多