【问题标题】:Swallowing exception thrown in catch/finally block在 catch/finally 块中抛出吞咽异常
【发布时间】:2009-10-31 14:13:00
【问题描述】:

通常我遇到的情况是,我必须吞下 catch/finally 块中的清理代码抛出的异常,以防止吞下原始异常。

例如:

// Closing a file in Java
public void example1() throws IOException {
    boolean exceptionThrown = false;
    FileWriter out = new FileWriter(“test.txt”);
    try {
        out.write(“example”);
    } catch (IOException ex) {
        exceptionThrown = true;
        throw ex;
    } finally {
        try {
            out.close();
        } catch (IOException ex) {
            if (!exceptionThrown) throw ex;
            // Else, swallow the exception thrown by the close() method
            // to prevent the original being swallowed.
        }
    }
}

// Rolling back a transaction in .Net
public void example2() {
    using (SqlConnection connection = new SqlConnection(this.connectionString)) {
        SqlCommand command = connection.CreateCommand();
        SqlTransaction transaction = command.BeginTransaction();
        try {
            // Execute some database statements.
            transaction.Commit();
        } catch {
            try {
                transaction.Rollback();
            } catch {
                // Swallow the exception thrown by the Rollback() method
                // to prevent the original being swallowed.
            }
            throw;
        }
    }
}

假设记录任何异常不是方法块范围内的选项,而是由调用example1()example2() 方法的代码完成。

吞下close()Rollback() 方法抛出的异常是个好主意吗?如果不是,那么有什么更好的方法来处理上述情况,以免异常被吞没?

【问题讨论】:

  • 我使用的模式与您的example1 几乎完全相同。
  • 对于实现IDisposable(通常是这种情况)的资源,我有implemented an extension method,它结合了您的example1 的逻辑以及其他策略。

标签: c# java


【解决方案1】:

我不喜欢捕获和重新抛出异常。

如果你抓住它,用它做一些事情 - 即使它只是记录异常。

如果你不能用它做任何事情,不要抓住它 - 在方法签名中添加一个 throws 子句。

捕获异常告诉我,要么您可以处理异常情况并制定恢复计划,要么“责任止步于此”,因为异常无法以这种形式传播得更远(例如,没有堆栈跟踪回给用户) .

【讨论】:

  • 同意。不要考虑如何结束通话 - 考虑向可怜的开发人员提供相关信息,他们必须仅根据日志修复晦涩且不可重现的错误。
  • 不同意 - 有时“用它做某事”重新抛出它 - 特别是捕获检查异常但将更有意义的未检查异常传递给客户端代码(我相信这种情况适用但是,仅适用于 Java)。 Spring 的 JdbcTemplate 是捕获已检查异常并将其公开为更有意义的未检查异常的绝佳示例。
  • @MetroidFan2002 - 我认为他的意思是捕获然后重新抛出同一个异常对象,而不是抛出一个新对象。
  • @Metroid,我想你和肯已经总结了“责任止步于此”的案例之一。如果我捕获并重新抛出一个更有意义和/或未经检查的异常,我是说原始异常不能传播到应用程序的该层之外。
  • 发生异常时,当对象状态需要改变时,也可以捕获并重新抛出。
【解决方案2】:

您可以创建一个自定义的Exception 类型,它可以容纳这两个异常。如果您重载ToString(),您可以记录这两个异常。

try
{
    transaction.Commit();
}
catch(Exception initialException)
{
    try
    {
        transaction.Rollback();
    }
    catch(Exception rollbackException)
    {
        throw new RollbackException(initialException, rollbackException);
    }

    throw;
}

【讨论】:

  • 回滚的要点是提交中发生了一些不好的事情,而您正在通过回滚处理它。有人可能会争辩说第一个异常是由 Rollback 处理的,而您不需要 initialException。如果回滚成功,您将永远看不到第一个异常。只有当它失败时,你才能看到两者。 Anywho,我同意新的异常类型。如果您想查看多个异常,请捕获两者并放入新的异常中。好主意
【解决方案3】:

这正是 Commons IO 具有 IOUtils.closeQuietly 方法的原因。在大多数情况下,关闭文件期间出现的问题并不那么有趣。

必须回滚的数据库事务可能更有趣,因为在这种情况下,函数没有做它应该做的事情(把东西放在数据库中)。

【讨论】:

    【解决方案4】:

    没有理由在 C# 代码中回滚事务。如果您关闭连接而不回滚(或提交)等效且更有效的事务...

    public void example2() {
      using (SqlConnection connection = new SqlConnection(this.connectionString))
      using (SqlCommand command = connection.CreateCommand())
      using (SqlTransaction transaction = command.BeginTransaction()) {
        // Execute some database statements.
        transaction.Commit();
      }
    }
    

    你就完成了。

    using 语句将确保(通过 finally)无论任何异常都关闭连接,并让原始异常冒泡(使用完整/正确的堆栈跟踪)。如果在调用 Commit 之前发生异常,则事务将永远不会提交,并且会在事务/连接关闭时自动回滚。

    【讨论】:

      【解决方案5】:

      我相信异常应该是您没有预料到的。如果您期望出现异常,那么您应该对其进行处理。因此,在您的第一个示例中,如果您还声明您的方法将抛出 IOException,我认为您可能不应该打扰捕获 IOException。

      【讨论】:

        【解决方案6】:

        我会考虑重写example1如下:

        // Closing a file in Java
        public void example1() throws IOException {
            boolean success = false;
            FileWriter out = new FileWriter(“test.txt”);
            try {
                out.write(“example”);
                success = true;
                out.close();
            } finally {
                if (!success) {
                    try {
                        out.close();
                    } catch (IOException ex) {
                        // log and swallow.
                    }
                }
            }
        }
        

        success = true; 移动到out.close(); 语句之后将使success 的含义更加清晰......尽管它可能导致out.close() 被调用两次。

        【讨论】:

          【解决方案7】:

          在不了解您的所有特定情况的情况下,您可以考虑抛出一个新异常。至少在 C# 中,当抛出一个新异常时,您的可选构造函数之一接受现有异常作为参数。例如:

          throw new Exception("This is my new exception.", ex);
          

          这样做的目的是保留原来的异常。

          另一个选项可能是 try .. catch .. finally 构造。

          试试{ // 可能抛出异常的普通代码 } 捕捉(异常前){ // 处理第一个异常 } 最后 { // 处理任何清理,无论是否抛出异常 }

          一般来说,如果我的代码可以处理特定 try .. catch 中的异常,那么我不会重新抛出该异常。如果调用堆栈上的某个异常很重要,我将抛出一个新异常并将原始异常设置为内部异常。

          【讨论】:

            【解决方案8】:

            通常,有风险的代码all放在一个 try-catch 块中。嵌套的 try-catch 块不是一个好主意,IMO(或者只是尽量避免嵌套的 try-catch 块,除非你真的需要它们)。

            因为有风险的代码是例外情况,所以将例外代码用于例外情况,甚至更多例外情况,这是很多不必要的工作。

            例如在example1() 中,将所有有风险的代码放在一个try-catch 块中:

            try{
            FileWriter out = new FileWriter(“test.txt”);
            out.write(“example”);
            out.close();
            } catch(Exception e) {
                //handle exception
            }
            

            或者,另一个好主意是为同一次尝试放置多个捕获:

            try{
                FileWriter out = new FileWriter(“test.txt”);
                out.write(“example”);
                out.close();
            } catch(FileNotFoundException e) {
                    //if IOException, do this..
            } catch(IOException e) {
                    //if FileNotFound, do this..
            } catch(Exception e) {
                    //completely general exception, do this..
            }
            

            【讨论】:

            • 您可以使用此代码泄漏文件句柄。如果write 抛出,close 将不会被调用。
            猜你喜欢
            • 1970-01-01
            • 2012-08-22
            • 2010-10-03
            • 2021-09-09
            • 1970-01-01
            • 2017-10-03
            • 2016-01-21
            相关资源
            最近更新 更多