尝试/捕获/记录/重新抛出-是反模式吗?


19

我可以看到几篇文章,其中强调了在中心位置或过程边界处处理异常的重要性,这是一种好的做法,而不是乱丢try / catch周围的每个代码块。我坚信我们大多数人都了解它的重要性,但是我仍然看到人们仍然使用catch-log-threrow反模式,主要是因为在任何异常情况下,为了简化故障排除工作,他们想记录更多特定于上下文的信息(例如:方法参数)通过),方法是将方法包装在try / catch / log / rethrow周围。

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

在保持异常处理良好实践的同时,有没有正确的方法来实现这一目标?我听说过类似PostSharp的AOP框架,但是想知道这些AOP框架是否有任何不利或主要的性能成本。

谢谢!


6
将每种方法包装在try / catch中,记录异常并让代码随处变化​​之间有巨大的区别。并尝试/捕获并使用其他信息重新抛出异常。首先是可怕的做法。其次,是改善调试体验的绝佳方法。
欣快的

我是说尝试/捕获每个方法,然后简单地登录catch块并重新抛出-这样可以吗?
rahulaga_dev

2
正如阿蒙指出的那样。如果您的语言具有堆栈跟踪,则登录每个捕获都是没有意义的。但是包装异常并添加其他信息是一个好习惯。
欣快的

1
请参阅@Liath的答案。我给出的任何答案都可以大致反映出他的观点:尽早捕获异常,如果在此阶段您所能做的就是记录一些有用的信息,然后进行重新抛出。在我看来,将其视为反模式是荒谬的。
David Arno

1
谎言:添加了小代码段。我正在使用C#
rahulaga_dev,

Answers:


19

问题不在于本地catch块,而是日志和重新抛出。处理该异常或将其包装为新异常,以添加其他上下文并将其引发。否则,您将遇到多个重复的日志条目,以获取相同的异常。

这里的想法是增强调试应用程序的能力。

范例1:处理

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

如果您处理异常,则可以轻松地降低异常日志条目的重要性,并且没有理由将该异常遍及整个链。已经处理了。

处理异常可以包括通知用户发生了问题,记录事件或只是忽略它。

注意:如果您有意忽略异常,我建议在空的catch子句中提供注释,以明确说明原因。这使将来的维护者知道这不是错误或懒惰的编程。例:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

示例2:添加其他上下文并抛出

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

假设存在问题,添加其他上下文(例如解析代码中的行号和文件名)可以帮助增强调试输入文件的能力。这是一种特殊情况,因此将异常重新包装在“ ApplicationException”中只是为了重新命名它并不能帮助您调试。确保添加其他信息。

例子3:除了异常什么都不做

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

在这最后一种情况下,您只允许离开异常而不接触它。最外层的异常处理程序可以处理日志记录。该finally子句用于确保清除您的方法所需的任何资源,但这不是记录引发异常的地方。


我喜欢“ 问题不是本地捕获块,问题是日志并重新抛出 ”,我认为这对于确保更干净的日志记录是很有意义的。但是最终这还意味着可以将try / catch分散在所有方法中,对吗?我认为必须有一些指导方针,以确保明智地遵循这种做法,而不是采用每种方法。
rahulaga_dev

我在回答中提供了指南。这不能回答您的问题吗?
Berin Loritsch '18

@rahulaga_dev我认为没有指导方针/银色的项目符号,因此请解决此问题,因为它很大程度上取决于上下文。没有通用的准则可以告诉您在哪里处理异常或何时重新抛出异常。IMO,我看到的唯一指导原则是将日志记录/处理推迟到最新的时间,并避免登录可重用的代码,以免造成不必要的依赖。如果您记录了事情(即处理的异常)而不给他们机会以自己的方式处理代码,则代码的用户不会太开心。只是我的两分钱:)
andreee '19

7

我不相信本地捕获是一种反模式,实际上,如果我没记错的话,它实际上是在Java中强制执行的!

对于我来说,实施错误处理的关键是整体策略。您可能需要一个过滤器来捕获服务边界处的所有异常,您可能想要手动拦截它们-只要有一个整体策略,这两个都很好,这将属于您团队的编码标准。

就个人而言,我可以在执行以下操作之一时捕获函数内部的错误:

  • 添加上下文信息(例如对象的状态或正在发生的事情)
  • 安全地处理异常(例如TryX方法)
  • 您的系统越过服务边界并调用外部库或API
  • 您想捕获并抛出其他类型的异常(也许将原始异常作为内部异常)
  • 该异常是作为某些低价值后台功能的一部分引发的

如果不是这些情况之一,则不添加本地try / catch。如果是这样,则根据情况,我可以处理异常(例如,返回错误的TryX方法)或重新抛出异常,以便全局策略可以处理该异常。

例如:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

或重新抛出示例:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

然后,您可以在堆栈顶部捕获该错误,并向用户显示一个友好的用户友好消息。

无论采用哪种方法,在这种情况下都值得创建单元测试,因此您可以确保功能不会更改,并且以后不会干扰项目的流程。

您没有提到您使用的是哪种语言,而是成为.NET开发人员,而且已经看到很多次了,更不用说了。

不要写:

catch(Exception ex)
{
  throw ex;
}

采用:

catch(Exception ex)
{
  throw;
}

前者重置堆栈跟踪,使您的顶级捕获完全无用!

TLDR

本地捕获不是一种反模式,它通常可以是设计的一部分,并且可以帮助向错误添加其他上下文。


3
当在顶级异常处理程序中使用同一记录器时,记录捕获的意义是什么?
欣快的

您可能无法在堆栈顶部访问其他信息(例如局部变量)。我将更新示例进行说明。
Liath

2
在这种情况下,请抛出带有其他数据和内部异常的新异常。
欣快的

2
@Euphoric是的,我也看到了,我个人也不喜欢它,因为它要求您为几乎每种方法/场景创建新的异常类型,我觉得这是很多开销。在此处添加日志行(大概在顶部添加一条日志)有助于说明诊断问题时的代码流程
Liath

4
Java不会强迫您处理该异常,而是会迫使您意识到它。您既可以捕获它并执行任何操作,也可以仅将其声明为函数可以抛出的内容,而在函数中不对其进行任何操作。
Newtopian

4

这在很大程度上取决于语言。例如,C ++在异常错误消息中不提供堆栈跟踪,因此通过频繁的catch-log-throw跟踪异常可以提供帮助。相反,Java和类似语言提供了很好的堆栈跟踪,尽管这些堆栈跟踪的格式可能不是很可配置的。除非您可以真正添加一些重要的上下文(例如,将低级SQL异常与业务逻辑操作的上下文连接),否则用这些语言捕获和重新抛出异常是毫无意义的。

通过反射实现的任何错误处理策略几乎都必须比该语言内置的功能效率低。此外,普遍日志记录具有不可避免的性能开销。因此,您确实需要在获取的信息流与该软件的其他要求之间取得平衡。就是说,基于编译器级工具构建的PostSharp之类的解决方案通常比运行时反射要好得多。

我个人认为记录所有内容并没有帮助,因为其中包含大量无关的信息。因此,我对自动化解决方案表示怀疑。给定一个好的日志记录框架,有一个商定的编码指南可能就足够了,该指南讨论您要记录的信息类型以及该信息应如何格式化。然后,可以在重要位置添加日志记录。

登录业务逻辑比登录实用程序功能重要得多。收集实际崩溃报告的堆栈跟踪信息(仅需要在进程的最高级别进行日志记录),您就可以找到代码中日志记录最具价值的区域。


4

当我看到try/catch/log每种方法时,都会引起人们的担忧,即开发人员不知道他们的应用程序可能会发生或可能不会发生什么,他们假设最坏的情况,并且由于他们期望的所有错误而抢先记录了所有内容。

这是一个现象,即单元和集成测试不足,开发人员习惯于在调试器中逐步执行大量代码,并希望通过某种方式进行大量日志记录,以便他们可以在测试环境中部署错误代码并通过查看问题来查找问题。日志。

该代码抛出异常可以比冗余代码,捕获和记录异常更为有用。如果在方法接收到意外的参数(并将其记录在服务边界)时引发带有有意义的消息的异常,则比立即记录作为无效参数的副作用而引发的异常并不得不猜测导致该异常的副作用要有用得多。

空值就是一个例子。如果您将值作为参数或方法调用的结果,但不应为空,则抛出异常。NullReferenceException由于空值,不要在以后仅记录结果抛出的五行。无论哪种方式,您都会获得例外,但是一个告诉您某件事,而另一种则使您寻找某事。

就像其他人所说的那样,最好将异常记录在服务边界上,或者每当因为优雅地处理异常而没有重新抛出异常时,都将其记录下来。最重要的区别是什么都没有。如果您的例外记录在一个容易找到的地方,您会在需要时找到所需的信息。


谢谢斯科特。您提出的要点是:如果在方法接收到意外参数时将异常抛出并发出有意义的消息(并将其记录在服务边界) ”,确实可以解决我在方法参数上下文中徘徊的情况。我认为在这种情况下拥有安全的防范条款并抛出ArgumentException是有意义的,而不是依赖于捕获和记录参数详细信息
rahulaga_dev

斯科特,我也有同样的感觉。每当我看到日志并重新记录仅用于记录上下文时,我都觉得开发人员无法控制类的不变式或无法维护方法调用。取而代之的是,所有方法始终都包裹在类似的try / catch / log / throw中。而且太可怕了。
Max

2

如果您需要记录不在异常中的上下文信息,则将其包装在新的异常中,并将原始异常提供为InnerException。这样,您仍然可以保留原始堆栈跟踪。所以:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Exception构造函数的第二个参数提供了一个内部异常。然后,您可以将所有异常记录在一个位置,并且仍然在同一日志条目中获得完整的堆栈跟踪上下文信息。

您可能要使用自定义异常类,但要点是相同的。

try / catch / log / rethrow很混乱,因为它将导致混乱的日志-例如,如果在上下文信息中记录另一个消息并在顶级处理程序中记录实际的异常之间发生另一个异常,该怎么办?如果新异常将信息添加到原始异常中,则try / catch / throw很好。


那么原始异常类型呢?如果我们包好,它就不见了。这是个问题吗?有人依靠SqlTimeoutException作为实例。
Max

@Max:原始异常类型仍然可以作为内部异常使用。
JacquesB

那就是我的意思!现在,所有正在调用SqlException的调用堆栈上的每个人都将永远无法获取它。
Max

1

异常本身应提供正确记录所需的所有信息,包括消息,错误代码以及不包含的信息。因此,应该没有必要仅将异常重新抛出或引发其他异常就捕获异常。

通常,您会看到几种捕获并重新抛出的异常作为普通异常的模式,例如捕获DatabaseConnectionException,InvalidQueryException和InvalidSQLParameterException并重新抛出DatabaseException。虽然如此,我还是认为所有这些特定的异常应该首先从DatabaseException派生,因此不需要重新抛出。

您会发现,删除不必要的try catch子句(即使是纯粹用于日志记录的子句)实际上也会使工作更轻松,而不是更难。只有程序中处理该异常的位置才应记录该异常,并且在所有其他操作均失败的情况下,应在整个程序范围内的异常处理程序中进行最后一次尝试记录该异常,然后再正常退出该程序。异常应具有完整的堆栈跟踪,以指示引发异常的确切点,因此通常不必提供“上下文”日志记录。

也就是说,AOP对您来说可能是一种快速修复的解决方案,尽管它通常会导致总体速度稍有下降。我鼓励您改为在不增加任何价值的情况下完全删除不必要的try catch子句。


1
异常本身应提供适当日志记录所需的所有信息,包括消息,错误代码以及不应该包含的信息 ”,它们应该,但实际上,它们不将Null引用为经典情况。例如,我不知道有什么语言可以告诉您在复杂表达式内导致该变量的变量。
大卫·阿诺

1
@DavidArno是的,但是您可以提供的任何上下文都不是那么具体。否则你会的try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }。在大多数情况下,堆栈跟踪就足够了,但是如果没有这种情况,错误类型通常就足够了。
尼尔,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.