如何避免异常阴影?

Posted

技术标签:

【中文标题】如何避免异常阴影?【英文标题】:How to avoid exception shadowing? 【发布时间】:2020-08-27 08:08:22 【问题描述】:

在处理异常的过程中发生异常时,只报告最后一个异常,因为我可以将一个异常添加到Error 对象。如何在最终的错误信息中报告所有异常?

例子:

class main

  public static void main (String[] args)
  
    try 
      // Database.insert ();
      throw new Exception ("insert failed");
    
    catch (Exception ex1) 
      try 
        // Database.rollback ();
        throw new Exception ("rollback failed");
      
      catch (Exception ex2) 
        throw new Error ("Can not roll back transaction.", ex2);
      
    
    finally 
      try 
        // Database.close ();
        throw new Exception ("close failed");
      
      catch (Exception ex3) 
        throw new Error ("Can not close database.", ex3);
      
    
  

在示例中,一切都失败了。数据库插入导致ex1。回滚导致ex2。关闭数据库会导致ex3。当程序执行时,只有最后一个 ex3 会在 Error 对象中报告。如何将ex1ex2 也包含到Error 对象中? Error 的构造函数只接受一个异常。

【问题讨论】:

您可以创建包含异常列表的自定义异常类吗?并继续更新并重新抛出? 【参考方案1】:

我建议您将 Java 7 中引入的 try-with-resource-statements 与 AutoCloseable-interface 结合使用。

旁注java.sql 中的ConnectionStatementResultSet 都实现了AutoCloseable

try (Connection c = DriverManager.getConnection(url)) 
    // do your inserts
 catch (Exception e) 
    throw new Error("Insert failed", e);

这将适当地关闭您的connection 或通常通过的AutoCloseable。并将使用Throwable.addSuppressed() 方法处理异常的阴影。

可以在this other question上看到之前版本中的等效外观


您的问题还提到了我没有涉及的回滚。这可以通过使用前面提到的 Throwable.addSuppressed() 方法(如 tobias_k 的 pointed out in the comments)来完成,但老实说它变得有点混乱,而且看起来不再那么漂亮了:

Exception insertException = null;
try (Connection c = DriverManager.getConnection(url)) 
    try 
        // do your inserts
     catch (Exception e1) 
        insertException = e1;
        // do your rollback
    
 catch (Exception e2) 
    Error error = new Error("Insert failed", insertException);
    error.addSuppressed(e2);
    throw error;

只是为了澄清,内部catch-block,只有在插入失败时才能到达。当外部的catch 可以到达时,当任何 抛出异常时:

DriverManager.getConnection() 您的回滚 Connection.close()

对于小型演示,您可以访问此link,它说明了堆栈跟踪的外观。

【讨论】:

恕我直言,更一般的答案是使用 addSuppressed 添加“阴影”异常,并且在许多情况下可以使用 AutoCloseable 来为您完成所有这些工作。 @tobias_k 如何在我的代码中使用addSuppressed?当我处理异常时,我不知道它是否会被稍后发生的异常抑制。 如何知道AutoCloseable代码中的异常处理是如何工作的?以及如何确定较早的异常确实被添加到抑制异常列表中? @Lino 我不明白为什么“回滚”会抑制“关闭”。对我来说,两种报告替代方案似乎是可行的:要么报告第一个压制后者,要么报告最后压制前者。但是报告中间的那个会抑制第一个和最后一个看起来有点混乱。 @ceving 你是对的,我已经改变了压制的顺序。现在看起来像 this【参考方案2】:

您所指的在Java中称为suppressed exceptions。

从 Java SE 7 开始,有一个 try-with-resources statement 可以自动处理其中抛出的异常。在您的示例中,它可以这样使用:

class main

  public static void main (String[] args)
  
    try(Database db = new Database()) //ex3 can be thrown during closing the resource
      try
      
        // db.insert ();
        throw new Exception ("insert failed");
      
      catch (Exception ex1) 
        try 
          // db.rollback ();
          throw new Exception ("rollback failed");
        
        catch (Exception ex2) 
          throw new Error ("Can not roll back transaction.", ex2);
        
      
    
  

在这种情况下,如果ex1ex3 被抛出,您会在抑制的异常列表中得到ex1ex3

如果ex1ex2ex3 被抛出,你会得到ex1 chained 和ex2ex3 在抑制的异常列表中。

【讨论】:

【参考方案3】:

我根据 Lino 的 answer 更新了我的代码。

class main

  public static void main (String[] args)
  
    Throwable suppressed = null;

    try 
      // Database.insert ();
      if ("fail".equals(args[0]))
        throw new Exception ("insert failed");
    
    catch (Exception ex1) 
      suppressed = ex1;
      try 
        // Database.rollback ();
        if ("fail".equals(args[1]))
          throw new Exception ("rollback failed");
        throw new Error ("Can not insert into database.", ex1);
      
      catch (Exception ex2) 
        ex2.addSuppressed (suppressed);
        suppressed = ex2;
        throw new Error ("Can not roll back transaction.", ex2);
      
    
    finally 
      try 
        // Database.close ();
        if ("fail".equals(args[2]))
          throw new Exception ("close failed");
      
      catch (Exception ex3) 
        if (suppressed != null)
          ex3.addSuppressed (suppressed);
        throw new Error ("Can not close database.", ex3);
      
    
  

现在所有四种不同的故障都已正确报告。

    插入失败
$ java -cp . main fail ok ok
Exception in thread "main" java.lang.Error: Can not insert into database.
        at main.main(main.java:18)
Caused by: java.lang.Exception: insert failed
        at main.main(main.java:10)
    插入和回滚失败
$ java -cp . main fail fail ok
Exception in thread "main" java.lang.Error: Can not roll back transaction.
        at main.main(main.java:23)
Caused by: java.lang.Exception: rollback failed
        at main.main(main.java:17)
        Suppressed: java.lang.Exception: insert failed
                at main.main(main.java:10)
    插入、回滚和关闭失败
$ java -cp . main fail fail fail
Exception in thread "main" java.lang.Error: Can not close database.
        at main.main(main.java:35)
Caused by: java.lang.Exception: close failed
        at main.main(main.java:30)
        Suppressed: java.lang.Exception: rollback failed
                at main.main(main.java:17)
                Suppressed: java.lang.Exception: insert failed
                        at main.main(main.java:10)
    关闭失败
$ java -cp . main ok ok fail
Exception in thread "main" java.lang.Error: Can not close database.
        at main.main(main.java:35)
Caused by: java.lang.Exception: close failed
        at main.main(main.java:30)

【讨论】:

【参考方案4】:

为什么您可能需要这样的结构?一般来说,我首先建议您使用 try-with-resources。它将显着简化您的代码。其次,要处理这种情况,您必须设计异常层次结构,因此您不会到处使用 Exception 类,这显然是反模式,但会抛出一种新类型的异常,例如而不是抛出新的异常(“关闭失败”),定义了将继承异常的CloseFailedException,并抛出它(抛出新的CloseFailedException(“关闭失败”))。

【讨论】:

我写了一个minimal reproducible example,为了尽量减少它,我避免写一堆异常类。我也没有在数据库中插入任何东西。

以上是关于如何避免异常阴影?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确命名变量以避免在Python中出现类似“外部作用域的阴影名称”的警告

虚幻引擎UEUE5 阴影异常与优化

矩形周围的阴影实现

如何在拖动时删除单元格的阴影?

高度和阴影

div盒子存在阴影导致父级标签出现滚动条