为什么要重新抛出异常?

时间:2020-03-06 14:31:38  来源:igfitidea点击:

我已经多次看到以下代码:

try
{
    ... // some code
}
catch (Exception ex)
{
    ... // Do something
    throw new CustomException(ex);

    // or
    // throw;

    // or
    // throw ex;
}

我们能否解释一下重新引发异常的目的?是否遵循异常处理中的模式/最佳实践? (我在某处读过它称为"呼叫者通知"模式?)

解决方案

在开始使用EntLib ExceptionBlock之前,我一直在使用它们来记录错误,然后再抛出它们。当我们认为我可以在那时处理它们时,有点讨厌,但那时最好是让它们在UAT中严重失败(在记录它们之后),而不是掩盖一个上流的bug。

应用程序很可能会在调用堆栈的更高层捕获这些重新抛出的异常,因此重新抛出它们将允许更高层的处理程序在适当时进行拦截和处理。对于应用程序来说,拥有一个顶级异常处理程序来记录或者报告期望是很常见的。

另一种选择是,编码器是惰性的,他们不仅捕获了要处理的异常集,还捕获了所有内容,然后仅重新抛出了无法实际处理的异常。

如果我们想记录但不处理该异常,则重新抛出相同的异常很有用。

抛出一个新的异常,该异常将捕获的异常包装起来,对抽象很有帮助。例如,图书馆使用的第三方图书馆会引发图书馆的客户不应该知道的异常。在这种情况下,我们可以将其包装到库更本地的异常类型中,然后将其抛出。

通常,"执行某些操作"或者更好地解释异常(例如,将其包装在另一个异常中),或者通过特定来源跟踪信息。

另一种可能性是,如果异常类型的信息不足以知道是否需要捕获异常,则在这种情况下对其进行检查将提供更多信息。

这并不是说使用此方法纯粹是出于充分的理由,很多时候,当开发人员认为将来可能需要跟踪信息时,都会使用该方法,在这种情况下,我们可以使用try {} catch {throw;}样式,即完全没有帮助。

我认为这取决于我们要对异常进行的处理。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。的"的。

一个很好的理由是首先将错误记录在catch中,然后将其扔到UI上以生成友好的错误消息,并带有选项以查看错误的更"高级/详细"视图,其中包含原始错误。

另一种方法是"重试"方法,例如,保留错误计数,并且在经过一定数量的重试之后,这是唯一一次将错误发送到堆栈上(有时是为了对超时的数据库调用进行数据库访问而完成的,或者通过慢速网络访问Web服务)。

不过,还有很多其他原因可以做到这一点。

重新抛出异常的主要原因是保持调用堆栈不变,因此我们可以更全面地了解发生的情况和调用顺序。

仅供参考,这是有关每种重新投掷类型的相关问题:
引发异常的性能注意事项

我的问题集中在"为什么"我们重新抛出异常及其在应用程序异常处理策略中的用法。

通常,我们捕获和重新抛出该异常是出于两个原因之一,具体取决于代码在应用程序中在体系结构中的位置。

在应用程序的核心,我们通常会捕获并重新抛出该异常,以将异常转换为更有意义的内容。例如,如果要编写数据访问层并在SQL Server中使用自定义错误代码,则可以将SqlException转换为ObjectNotFoundException之类的东西。这很有用,因为(a)使调用者更容易处理特定类型的异常,以及(b)因为它阻止了该层的实现细节,例如我们使用SQL Server进行持久性泄漏到其他层的事实,让我们将来更轻松地进行更改。

在应用程序的边界处,捕获和重新抛出而不转换异常是很常见的,这样我们就可以记录它的详细信息,以帮助调试和诊断实时问题。理想情况下,我们希望将错误发布到操作团队可以轻松监视的位置(例如事件日志),以及为开发人员提供控制流中发生异常的位置的上下文(通常是跟踪)。

其实两者之间是有区别的

throw new CustomException(ex);

throw;

第二个将保留堆栈信息。

但是有时我们想使Exception对应用程序域更加"友好",而不是让DatabaseException到达GUI,而是引发包含原始异常的自定义异常。

例如:

try
{

}
catch (SqlException ex)
{
    switch  (ex.Number) {
        case 17:
        case 4060:
        case 18456:
           throw new InvalidDatabaseConnectionException("The database does not exists or cannot be reached using the supplied connection settings.", ex);
        case 547:
            throw new CouldNotDeleteException("There is a another object still using this object, therefore it cannot be deleted.", ex);
        default:
            throw new UnexpectedDatabaseErrorException("There was an unexpected error from the database.", ex);
    } 
}

我可以想到以下原因:

  • 作为API的一部分,将抛出的异常类型集保持固定,以便调用者仅需担心固定的异常集。在Java中,由于检查异常机制,我们实际上被迫这样做。
  • 向异常添加一些上下文信息。例如,我们可能希望捕获它并添加" ...,同时处理编号为XXX的订单,寻找产品YYY",而不是让裸露的"未找到记录"从数据库中通过。
  • 做一些清理-关闭文件,回滚事务,释放一些句柄。

有时我们想隐藏方法的实现细节或者进行改进
问题的抽象级别,以使其对调用者更有意义
一种方法。为此,我们可以拦截原始异常并替换为
一个更适合于解释问题的自定义异常。

例如,一种从文本文件加载请求的用户详细信息的方法。该方法假定存在一个文本文件,该文件以用户ID和后缀.data命名。当该文件实际上不存在时,抛出FileNotFoundException并没有多大意义,因为每个用户详细信息都存储在文本文件中的事实是该方法内部的实现详细信息。因此,该方法可以将原始异常包装在带有说明性消息的自定义异常中。

与显示的代码不同,最佳实践是应将原始异常作为新异常的InnerException属性加载,以保留原始异常。这意味着开发人员仍然可以在必要时分析潜在的问题。

创建自定义例外时,这是一个有用的清单:

?找到一个很好的名称,该名称可以说明引发异常的原因,并确保名称以单词Exception结尾。

?确保实现三个标准异常构造函数。

?确保使用Serializable属性标记异常。

?确保实现反序列化构造函数。

?添加任何可能有助于开发人员更好地理解和处理异常的自定义异常属性。

?如果添加任何自定义属性,请确保实现并覆盖GetObjectData以序列化自定义属性。

?如果添加任何自定义属性,请覆盖Message属性,以便可以将属性添加到标准异常消息中。

?请记住使用自定义异常的InnerException属性添加原始异常。

正如Rafal所提到的,有时这样做是为了将已检查的异常转换为未检查的异常,或者转换为更适合API的已检查的异常。这里有一个例子:

http://radio-weblogs.com/0122027/stories/2003/04/01/JavasCheckedExceptionsWereAMistake.html