我们有一项服务将在应用程序域级别记录未处理的异常(通过 Log4net)。
我们记录了:
2014-01-28 16:49:19,636 错误 [49] FeedWrapperService - 未处理 System.NullReferenceException:未将对象引用设置为对象的实例。
此异常没有堆栈跟踪。如果不对异常对象做一些疯狂的事情,这怎么可能?
我们的处理代码:
AppDomain.CurrentDomain.UnhandledException += LogAnyExceptions;
private void LogAnyExceptions(object sender, UnhandledExceptionEventArgs e)
{
log.Error("unhandled", (Exception)e.ExceptionObject);
throw (Exception)e.ExceptionObject;
}
我觉得这里的重新抛出是没有意义的,因为 AppDomain 无论如何都会随着进程一起下降,但我认为这不会影响我们的情况。
Windows 应用程序事件查看器也仅显示此空引用异常,没有任何跟踪。
我已经测试了异常处理程序日志记录,它成功记录了堆栈跟踪和任何内部异常。如果它是由我们的代码抛出的,我们会看到堆栈跟踪。如果它是由第 3 方 C# 库抛出的,那么我们将再次看到至少一个方法的堆栈跟踪(无论它是否是重新抛出的异常)。这里我们看到一个没有堆栈跟踪的托管异常。我不知道这怎么可能。
查看反编译的第 3 方库,它与非托管代码进行通信,引发此异常的事件很可能发生在非托管区域中,但是这种情况如何在没有堆栈跟踪的情况下导致托管空引用异常?
此问题的原因是间歇性的。我们已经在生产中运行这段代码几个月了,并且看到它执行过一次。真是太奇怪了。
普遍的共识是,应该将造成此类问题的系统推送到子进程中,以便我们可以处理问题并安全自动地重新启动,但最好知道发生了什么。
编辑以包含以下评论信息:
我的异常不是标准的重新抛出,因为堆栈跟踪要么为空,要么为空。它没有重新抛出方法的名称。进一步挖掘,可以从序列化信息构造 Exception 类,并且看起来序列化信息可以包含用于堆栈跟踪的空字符串,并且可能可以在不导致其他错误的情况下创建该字符串。我猜它可能来自那里,但我不知道它是如何起源的。
如果您收到异常但没有相应的堆栈跟踪,那么在某些时候异常处理程序可能正在评估异常并错误地重新抛出它。例如,如果您正在执行
throw ex;
,您将吃掉导致该点的堆栈跟踪。要保留现有的调用堆栈,您只需 throw;
抛出异常的最佳实践
请注意,C# 方式与 Java 语言的约定相反,您应该
throw ex;
Java 参考:最佳实践:捕获并重新抛出 Java 异常
我更喜欢使用自定义异常来管理我的异常,如果在您的情况下您使用自己的异常,您可以转到您定义它们的类并覆盖堆栈跟踪。例如:
public class YourCustomException: Exception
{
public YourCustomException() : base() { } //constructor
public override string StackTrace
{
get { return "Message instead stacktrace"; }
}
}
既然这个问题已经开放了足够长的时间,我会采取唯一可能的答案,而不是永远开放:)
即:
Exception 类可以从序列化信息构造,并且看起来序列化信息可以包含用于堆栈跟踪的空字符串,并且可能可以在不导致其他错误的情况下创建该字符串。我猜它可能来自那里,但我不知道它是如何起源的。
我唯一的另一个想法是,在应用程序域被拆除之前,其余的异常信息可能无法写入日志文件。但由于事件查看器中显示了相同的内容,并且我假设整个事件是自动创建的,所以我认为事情并非如此。
像这样创建常见异常类型:
/// <summary>
/// Special kind of exception, which is known and no need to report call stack on it's failure.
/// </summary>
class UIException: Exception
{
public UIException(string message): base(message)
{
}
/// <summary>
/// Don't display call stack as it's irrelevant
/// </summary>
public override string StackTrace
{
get {
return "";
}
}
}
然后就
throw new UIException("failure message");
当您得到
AggregateException
时,堆栈跟踪就是 null
。
这种异常就像一个桶,其中可以包含一个或多个具有不同堆栈跟踪的异常。
这种情况的解决办法:检查接收到的异常是否是该类型。如果是这样,则分别处理每个包含的异常。
简单的例子:
private void LogAnyExceptions(object sender, UnhandledExceptionEventArgs e)
{
if ((e.ExceptionObject is AggregateException aggEx) && (aggEx.InnerExceptions != null))
{
foreach (Exception innerAggEx in aggEx.InnerExceptions)
{
// do something with innerAggEx
}
}
else // single exception
{
// do something with e.ExceptionObject
}
}
更多回调和简单崩溃记录器的扩展示例:
应用回调:
注意:名称/类可能略有不同,与 .NET 版本和/或应用程序类型相关。 (此示例是使用 .NET 4.8 为 WPF 创建的)
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnUnhandledException);
Application.Current.DispatcherUnhandledException += new DispatcherUnhandledExceptionEventHandler(OnDispatcherUnhandledException);
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
添加回调方法和记录器:
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
string exitInfo = (args.IsTerminating == true) ? "application crash" : "application continues";
string info = $"Unhandled AppDomain exception ({exitInfo})";
LogUnhandledException(info, (Exception)args.ExceptionObject);
}
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
string info = $"Unhandled dispatcher exception (handled: {e.Handled})";
LogUnhandledException(info, e.Exception);
}
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
string info = $"Unhandled task exception (observed: {e.Observed})";
LogUnhandledException(info, e.Exception);
}
private void LogUnhandledException(string info, Exception e)
{
string crashMessage = $"{info} at {DateTime.Now:yyyy-MM-dd HH:mm:ss}\r\n"
+ $"Message(s): {GetAllExceptionMessages(e)}\r\n"
+ $"Exception type: {e.GetType().Name}\r\n"
+ $"Stack trace:\r\n{e.StackTrace ?? "[NULL]"}";
// attach aggregated exceptions if available
if ((e is AggregateException aggEx) && (aggEx.InnerExceptions != null))
{
int i = 1;
foreach (Exception innerAggEx in aggEx.InnerExceptions)
{
crashMessage += $"\r\n------------------------------ aggregation #{i++} ------------------------------\r\n"
+ $"Message(s): {GetAllExceptionMessages(innerAggEx)}\r\n"
+ $"Exception type: {innerAggEx.GetType().Name}\r\n"
+ $"Stack trace:\r\n{innerAggEx.StackTrace ?? "[NULL]"}";
}
}
// log to file
try
{
string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string randomString = Guid.NewGuid().ToString().Substring(0, 5); // get unique file name, even if different exception callbacks are fired at the same time
string fullFileName = Path.Combine(exeDir, $"CrashLog_{DateTime.Now:yyyy-MM-dd_HH.mm.ss_}{randomString}.txt");
File.WriteAllText(fullFileName, crashMessage);
}
catch
{
}
}
private string GetAllExceptionMessages(Exception e)
{
return (e == null) ? String.Empty : " | " + GetAllExceptionMessages(e.InnerException);
}