65.9K
CodeProject 正在变化。 阅读更多。
Home

确保错误仅记录一次

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.33/5 (5投票s)

2018 年 11 月 28 日

CPOL

4分钟阅读

viewsIcon

8920

通过利用.NET的一些相当普通的特性,我们可以在异常发生的地方记录错误;保留重要的调试信息,同时避免重复的错误日志记录。

引言

这是一个普通的try/catch块,你刚刚捕获了一个错误。你应该记录它吗?它已经被记录了吗?你肯定不想多次记录它,但是你怎么知道呢?

根据MSDN,这是一个真实的问题

避免在调用堆栈较低的位置进行异常报告或日志记录。
...
此类catch块不应在调用堆栈深处记录异常或将其报告给用户。如果异常被记录并重新抛出,则调用堆栈中较高的调用者可能会执行相同的操作,从而导致异常的重复日志条目……

这带来一个问题,因为最好在错误发生点附近记录错误,因为这样你就可以访问方法参数,你也可以记录这些参数来帮助你调试异常。但是根据上面的MSDN,这会导致重复的日志条目。

让我们看一些代码来说明这个问题

public void CreateBookings(string[] people)
{
	try
	{
		foreach (string person in people)
			CreateBookingForPerson(person);
	}
	catch (Exception e)
	{
		_logger.LogError(e);
	}
}

public void CreateBookingForPerson(string person)
{
	_bookingRepo.CreateBooking(person);
}

请注意,我们只在第一个方法中捕获和记录错误:在CreateBookingForPerson中发生的错误将冒泡并记录在CreateBookings方法中的catch块中。

现在假设我们要处理一个包含10个人的数组;在第四个项目中,在以下行发生错误

_bookingRepo.CreateBooking(person);

错误将冒泡直到被CreateBookings中的catch块捕获。问题在于:此时,我们不再访问导致错误的person项目:我们可以记录发生了错误,但我们丢失了原因(详细信息)。如果异常是在CreateBookingForPerson内部捕获的,我们仍然可以访问导致错误的哪个项目,这是调试问题的重要信息。

然而,当然,如果我们确实决定向CreateBookingsForPerson添加一个日志记录try/catch,错误最终会被记录两次:这是我们试图避免的。

解决方案

那么解决方案是什么呢?核心概念是:当捕获异常时

  1. 记录异常并将其包装在一个自定义异常类型LoggedException中。
  2. 抛出这个新创建的LoggedException
  3. 忽略/不记录类型为LoggedException的异常。

让我们首先看看LoggedException类的代码。

public class LoggedException : Exception
{
	public LoggedException(string message)
	: base(message)
	{
		LogException(message, null);
	}

	public LoggedException(string message, Exception innerException)
	: base(message, innerException)
	{
		LogException(message, innerException);
	}

	private void LogException(string message, Exception innerException)
	{
		//hook up logging framework of choice
	}
}

这个类相当简单;它只是围绕内置Exception类的包装器。错误详细信息的记录在实例化期间通过调用LogException发生。LogException是您将连接您选择的日志记录框架的地方。作为一个额外的好处,我们现在已经集中了我们的日志记录功能。如果我们想更换日志记录框架,唯一需要更改的代码就在LogException内部。

实际上,就是这样。然而,真正的魔力发生在LoggedException类的实现过程中。

实现

让我们再看看引言中的代码,但这次使用LoggedException

public void CreateBookings(string[] people)
{
	try
	{
		foreach (string person in people)
			CreateBookingForPerson(person);
	}
	catch (Exception e) when (!(e is LoggedException))
	{
		string errorMsg = "An error occurred while creating bookings.";
		throw new LoggedException(errorMsg, e);
	}
}

public void CreateBookingForPerson(string person)
{
	try
	{
		_bookingRepo.CreateBooking(person);
	}
	catch (Exception e) when (!(e is LoggedException))
	{
		string errorMsg = $"An error occurred while creating a booking for person {person}.";
		throw new LoggedException(errorMsg, e);
	}
}

一旦错误被记录,两个catch块都会抛出一个LoggedExceptionLoggedException告诉堆栈中较高的catch块,异常已经被记录。有了这个机制,我们的错误日志可以包含快速查明问题所需的额外细节。

你注意到每个catch块上的when关键字了吗?

catch(Exception e) <code>when</code> (!(e is LoggedException))

这些是异常筛选器,这是C# 6的主要新特性之一。它们允许你指定进入catch块的条件。乍一看,你可能会认为这只是一些额外的语法糖,但它远不止于此。这里的大问题是异常筛选器不会展开堆栈。这太重要了!细节超出了本文的范围,但是对于更深入的讨论,这篇文章做得很好。值得注意的是,实现异常筛选器不是必需的。

结论

通过利用.NET的一些相当普通的特性,我们可以完全避免重复记录错误这一相当麻烦的问题。这种方法能够访问异常发生时的局部变量,提供额外的细节来快速查明问题。

避免在调用堆栈较低的位置进行异常报告或日志记录。
...
此类catch块不应在调用堆栈深处记录异常或将其报告给用户。如果异常被记录并重新抛出,则调用堆栈中较高的调用者可能会执行相同的操作,从而导致异常的重复日志条目……

问题解决。

历史

  • 2018-11-28:初始版本
© . All rights reserved.