【问题标题】:Two exception management questions两个异常管理问题
【发布时间】:2012-02-16 11:38:05
【问题描述】:

我有几个关于网站异常管理的问题:

  • 在 catch 块中,我可以有一个带有异常处理方法的静态类,像这样处理异常:

       catch (Exception ex)
        {
            throw ExceptionHandler.HandleException(...);
        }
    

    其中 ExceptionHandler.HandleException 是一个静态方法,它返回一个 System.Exception 类型的变量。这是一个好习惯吗?这种方法有什么可能的问题吗?它是线程安全的吗?

  • 在我的应用程序中,我有一个由业务层调用的 DAL 层,而业务层由 UI 调用。那么,重新抛出所有自定义异常是否是一个好习惯,这样它们就会冒泡到显示它们的 UI 上,而 System.Exception 类型会被记录,并且我在 catch 块中抛出一个自定义异常? 例如在 DAL 和业务层中,如下所示:

        catch (CustomExceptionBase ex)
        {
            throw;
        }
        catch (Exception sysEx)
        {
            ICustomExceptionBase ex = new SysException(sysEx);
            ex.Handle();
            throw BusinessException("Some problem while serving your request");
        }
    

    在 UI 层这样

        catch (CustomExceptionBase ex)
        {
            //when custom exception bubbles up; code to display text to user on screen
        }
        catch (Exception sysEx)
        {
            ICustomExceptionBase ex = new SysException(sysEx);
            ex.Handle();
            //display error on screen;
        }
    

    这里CustomExceptionBase实现了ICustomExceptionBase,继承了Exception。 SysException 和 BusinessException 都继承自 CustomExceptionBase。

感谢您的宝贵时间...

编辑 在 system.Exceptions 块中重新抛出的目的是,如果出现致命错误,例如数据库连接丢失或类似情况,那么我将其记录到技术帮助台并返回票号并重新抛出相同的内容,以便用户知道发生了什么事错了,这是您要跟进的参考编号。对于 DAL 层或业务层中的所有自定义异常,我只是将其一直冒泡到显示文本的 UI。

【问题讨论】:

  • 嘿,如果你让它线程安全,它将是线程安全的。除此之外,我会说,如果您已经处理了异常,则不需要重新抛出它。如果您所做的只是记录异常并重新抛出它,那么 catch 块可能毫无意义。在我看来,处理异常意味着以不需要重新抛出的方式处理它。
  • 点了。但是这里系统异常的意图是 1.) 记录它,这就是 ex.Handle() 会做的所有事情 2.) 提醒用户出现问题,因此抛出..
  • 也许 Handle() 是一个误导性的名称...
  • 我认为处理所有可能的Exceptions 是不好的做法(除了一些警告用户并关闭应用程序的顶级代码)。只处理您知道如何处理的异常。
  • 我想优雅地处理所有错误,而不向用户显示 asp.net 黄色错误屏幕:)

标签: c# exception exception-handling


【解决方案1】:

我怀疑某些答案至少完全取决于您的架构。在第一种情况下,这完全取决于 ExceptionHandler.HandleException 的确切作用。它是根据某些标准生成一个新的异常,还是仅仅返回原始异常?

它的线程安全与否完全取决于它的实现。例如,在以下简单的情况下,我会说它是线程安全的:

public static Exception ExceptionHandler.HandleException(Exception ex)
{
    return ex;
}

在其他情况下,它很容易不是线程安全的。例如:

public static string message;
public static Exception ExceptionHandler.HandleException(Exception ex)
{
    message = ex.ToString;
    sleep(2000);
    return new Exception(message);
}

后一个示例显然具有在另一个线程处于睡眠状态时由另一个线程更改消息变量的范围。

至于第二个......应该在处理它们有意义的地方处理异常。没有硬性规定。如果代码的某些部分可以影响从异常中恢复(或愿意跳过它),则在该点而不是更早地捕获它。如果一个异常真的是致命的,那么什么都不应该试图捕捉它并假装不然,所以你应该让它冒泡到顶部并做一些事情,比如提醒你的用户事情已经崩溃,你需要重新启动或其他什么。

因此,这实际上取决于您的自定义异常的含义。如果它们只是表示“您想重试”,那么这与“数据完整性已受到损害:0==1”的异常不同。这两个都可能是自定义的,所以真正由您决定在哪里处理事情。

【讨论】:

  • 在第一种情况下,Handle 正在调用 log4net 来记录异常。在第二个示例中,如果出现致命错误,例如数据库连接丢失或类似情况,那么我将其记录到技术帮助台并返回票号并重新抛出相同的问题,以便用户知道出现问题,这是您要跟进的参考号。
  • 你的第一个问题就是 log4net 线程安全。我自己从未使用过它,但我怀疑它可能是。您可能还不想抛出从您的方法返回的内容,而只是 throw 以便原始异常与原始上下文一起传递。第二,这听起来像是一个明智的策略。很难对这样的事情给出明确的答案,因为这完全取决于个别情况。
【解决方案2】:

是的,您可以在 catch 块内调用静态异常处理程序,只要您不引用任何静态变量,它就可能是线程安全的。

您应该查看 Microsoft 的企业库。它具有几乎相同的设计,但使用 web.config 中定义的异常策略来控制异常的冒泡、包装或丢弃方式。将它与应用程序日志记录块结合起来,您就有了一个完整的解决方案。

【讨论】:

    【解决方案3】:

    使用静态方法来处理异常/重新抛出异常本身并没有任何技术问题,但是从最佳实践的角度来看,拥有一个神奇地“处理”异常的单一方法让我觉得潜在的代码异味。例外就其名称而言就是例外,每个单独的案例都需要仔细考虑以确保您做的是正确的事情,因此我发现您的 HandleException 方法不太可能总是做一些明智的事情。

    作为一个极端的例子,我知道一个这样的应用程序,其中几乎每个方法都包装在一个 try-catch 块中,并调用一个抛出通用 MyApplicationException 类型的静态异常处理程序方法。这是一个非常糟糕的主意:

    • 代码混乱
    • 这使得理解堆栈跟踪变得更加困难
    • 这使得调用者很难捕捉和处理特定的异常类型
    • 与以前相比,它使抛出异常的性能损失更大

    我最喜欢的是一个没有实现的方法,看起来有点像这样:

    void SomeException()
    {
        try
        {
            throw new NotImplementedException();
        }
        catch(Exception ex)
        {
            throw ExceptionHandler.HandleException(...);
        }
    }
    

    最糟糕的一点当然是它完全没有头脑。正如我之前所说的异常是异常的 - 每个try ... catch 块都需要仔细考虑并考虑它应该如何表现,使用通用HandleException 方法是一个立即警告标志,这可能不是这种情况。

    重新抛出异常

    一般来说,你应该只在两种情况下重新抛出异常:

    • 当您想将上下文信息添加到异常时(例如正在处理的当前文件的名称)
    • 当您必须捕获异常以处理某些特定情况时,例如handling an "out of disk space" error

      catch (IOException ex)
      {
          long win32ErrorCode = Marshal.GetHRForException(ex) & 0xFFFF;
          if (win32ErrorCode == ERROR_HANDLE_DISK_FULL || win32ErrorCode == ERROR_DISK_FULL)
          {
              // Specific "out of disk space" error handling code
          }
          else
          {
              throw;
          }
      }
      

    “冒泡”(即捕获并重新抛出异常而不用它做任何事情)是完全没有必要的——这就是异常已经被设计成可以自己做的事情!

    处理异常

    其他人说“应该在有意义的地方处理异常”,我什至自己也给出了这个建议,但事后看来,我认为这不是特别有用的建议! :)

    人们通常的意思是您应该处理特定原因的异常,并且您应该根据该原因在应用程序中选择处理该异常的位置。

    例如,如果您想显示一条错误消息以通知用户他们无权修改文件,如果您收到拒绝访问错误,那么您的 UI 代码中可能有一个特定的 try-catch 块这样做:

    catch (IOException ex)
    {
        long win32ErrorCode = Marshal.GetHRForException(ex) & 0xFFFF;
        if (win32ErrorCode == ERROR_ACCESS_DENIED)
        {
            // Display "access denied error"
        }
        else
        {
            throw;
        }
    }
    

    请注意,这对于我们希望处理的这种情况非常具体 - 它仅捕获感兴趣的特定异常类型执行额外检查以过滤到我们所处理的特定情况有兴趣。

    或者,如果您想记录未处理的错误或向用户优雅地显示错误消息而不是 IIS 505 错误屏幕,那么可以在 Global.asax 中或通过自定义错误页面 - ASP.Net Custom Error Pages

    我的观点是,在处理异常时,我们正在仔细考虑我们希望在应用程序功能方面实现什么(例如重试逻辑、错误消息、日志记录等......)并具体实现我们的异常处理逻辑尽可能以最有针对性的方式解决这些要求 - 没有神奇的异常处理框架,也没有样板代码。

    尽可能完全避免异常!

    我通常发现最好的策略就是尽可能完全避免异常!例如,如果您的页面解析用户输入的数字,并且您希望在他们输入愚蠢的值时显示验证消息,那么请预先验证您的输入,而不是捕获异常:

    不好:

    void DoSomething()
    {
        int age = int.Parse(ageTextBox.Text);
        if (age < 0)
        {
            throw new ArgumentOutOfRangeException("age must be positive");
        }
        if (age >= 1000)
        {
            throw new ArgumentOutOfRangeException("age must be less than 1000");
        }
    }
    
    void Button_Click(object sender, EventArgs e)
    {
        try
        {
            DoSomething();
        }
        catch (Exception ex)
        {
            DisplayError(ex.Message);
        }
    }
    

    好:

    void Button_Click(object sender, EventArgs e)
    {
        int age;
        if (!int.TryParse(ageTextBox.Text, out age))
        {
            DisplayError("Invalid age entered");
        }
        if (age < 0)
        {
            DisplayError("age must be positive");
        }
        if (age >= 1000)
        {
            DisplayError("age must be less than 1000");
        }
    
        DoSomething();
    }
    

    用户一直在输入无效数据 - 处理这实际上是应用程序逻辑,不应落入真正的异常处理 - 这当然不是我称之为“异常”的事件。

    当然这并不总是可行的,但是我发现使用这种策略可以简化代码并更容易遵循应用程序逻辑。

    【讨论】:

    • 感谢 Justin 抽出宝贵时间为我提供了非常详细的答案。虽然我消化了您输入的所有内容,但我脑海中浮现出一些问题:我不打算为可以在 UI 上验证的数据抛出异常。在我的 asp.net MVC 应用程序中,数据访问层处理某些文档时会返回一个错误代码,该代码被解释为:“文档中没有足够的详细信息”,然后我在 DAL 中抛出一个自定义异常,这就是这种情况一直冒泡到 UI 层。 IT 帮助台不会对调试此异常感兴趣,所以我没有记录它。
    • 但是,如果是数据库服务器宕机之类的错误,则会在 System.Exception 捕获块中捕获。现在在这里我做两件事:使用 [ex.Handle()] 将错误记录到文本文件或事件日志中,然后抛出带有票号或一些类似参考的自定义异常,这样用户就会被带到网站的错误包含该信息的页面。在 dal 层中抛出的这个异常在业务层中被重新抛出,现在它可以在 UI 中显示..
    • 您是否认为使用“throw;”冒泡异常是不好的做法? ,正确的方法是,每个方法都返回一个包含 ErrorString 属性的对象?我在某处读过抛出异常不会导致任何可测量的性能损失,所以我认为使用 throw new CustomExcpetion("") 冒泡异常而不是创建一个额外的对象来将错误字符串传递给界面层。你还有什么推荐的方法?
    • @user20358 在您的情况下,并行将重构您的 API 以在不使用异常的情况下报告“文档中的详细信息不足”的情况,例如通过让它返回一个“任务状态”对象,该对象封装了处理未完成的事实和指示原因的消息。很可能这太麻烦而收获太少,但是如果从未记录过此异常,那么当抛出此异常时,它听起来很像往常一样。然后,您可以全局处理其他异常(例如缺少数据库连接)。
    【解决方案4】:

    首先我们需要考虑异常类型,在任何业务中,异常都可以是BusinessException或Technical/SystemException。

    1. 在 BusinessException 中,我们可以发送带有错误的自定义异常 详情。

    2. 在技术/系统异常中,我们不应该处理它,而是让它 弹出到 UI 层。UI 可以决定应该显示什么错误 例外情况。

    3. 在您的方法中,如果您处理异常或抛出自定义 异常调用跟踪丢失。

    【讨论】:

    • 在自定义异常中,唯一需要设置的是编码器发送给类以进行设置的错误消息属性。如果业务层在用户输入中遇到一些异常,它会将其标记为: throw BusinessExcpetion("Your application was denied due to your document not have the right information");
    • 这是当业务层需要将一些文档发送给另一个服务进行处理时,该服务返回一个如上解释的错误。在这里,它只是一直冒泡到顶部。 IT 帮助台不会对调试此异常感兴趣
    • 当服务关闭时,我想告诉用户...嘿,某处出了点问题。我们已经记录了它,如果您想稍后与我们联系,这里是您的票号.. 所以在这种情况下 ex.Handle() 会将错误记录到文本文件或事件日志中,然后用票抛出业务异常附加编号,以便用户使用该信息进入网站的错误页面。
    • 在这种情况下,您可以记录错误详细信息(您可以使用 Microsoft.ENterpriseLibrary 或 log4net)。您可以向 UI 层抛出业务异常并显示相应的消息。\
    猜你喜欢
    • 2011-02-03
    • 1970-01-01
    • 2016-06-27
    • 1970-01-01
    • 2011-01-18
    相关资源
    最近更新 更多