832 字
4 分钟
实战:构建稳健的全局异常捕获与日志监控系统

在生产环境中,未经处理的异常会导致程序直接闪退(Crash),严重影响用户体验。为了实现“程序崩溃但不闪退”以及“错误可溯源”,我们需要建立一套完善的全局异常捕获与日志记录机制。

一、 全局异常捕获#

在 WPF 中,异常可能来源于三个地方:UI 线程非 UI 线程、以及 Task 异步任务。我们需要分别对这三者进行监听。

1.1 核心实现方案#

App.xaml.cs 中重写 OnStartup 方法,集中注册异常处理事件。

public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 1. 初始化日志系统
LogHelper.InitLog4Net();
// 2. 注册全局异常捕获
RegisterEvents();
}
private void RegisterEvents()
{
// 处理 UI 主线程未捕获的异常 (防止程序崩溃直接退出)
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
// 处理非 UI 线程(如子线程)未捕获的异常
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
// 处理 Task 任务内未被 Await 的异常
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
#region 异常处理回调
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
HandleException(e.Exception);
e.Handled = true; // 设置为 true,表示异常已处理,防止程序强制退出
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex)
{
HandleException(ex);
}
}
private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
HandleException(e.Exception);
e.SetObserved(); // 标记异常已被观察到,避免触发终结器导致的崩溃
}
private void HandleException(Exception ex)
{
if (ex == null) return;
// 记录日志
LogHelper.WriteErrLog("系统发生未捕获异常", ex);
// 友好提示(建议在 UI 线程弹出)
Current.Dispatcher.Invoke(() =>
{
MessageBox.Show($"抱歉,程序遇到了一些问题:{ex.Message}", "系统异常",
MessageBoxButton.OK, MessageBoxImage.Error);
});
}
#endregion
}

二、 使用 log4net 记录日志#

log4net 是 .NET 生态中最成熟的日志组件之一。它支持将日志分级别(Info/Warn/Error)存储。

2.1 日志帮助类 LogHelper#

using log4net;
using log4net.Config;
using System.IO;
public static class LogHelper
{
private static readonly ILog loginfo = LogManager.GetLogger("loginfo");
private static readonly ILog logerror = LogManager.GetLogger("logerror");
public static void InitLog4Net()
{
var logCfg = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config"));
XmlConfigurator.ConfigureAndWatch(logCfg);
}
public static void WriteInfoLog(string info)
{
if (loginfo.IsInfoEnabled) loginfo.Info(info);
}
public static void WriteErrLog(string info, Exception ex)
{
if (logerror.IsErrorEnabled) logerror.Error(info, ex);
}
}

2.2 配置文件 log4net.config (关键)#

建议将日志保存为 .txt.log。若要保存为 .htm,需要配合正确的 HTML 布局。

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="ErrorAppender" type="log4net.Appender.RollingFileAppender">
<file value="Logs/Error/" />
<appendToFile value="true" />
<rollingStyle value="Date" />
<staticLogFileName value="false" />
<datePattern value="yyyy-MM-dd'.log'" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%n=== 异常时间:%d [%t] ===%n级别:%-5p%n消息:%m%n堆栈:%exception%n%n" />
</layout>
</appender>
<logger name="logerror">
<level value="ERROR" />
<appender-ref ref="ErrorAppender" />
</logger>
</log4net>

三、 轻量级备选:自定义日志类#

如果你不希望引入第三方库,可以使用 ReaderWriterLockSlim 实现一个轻量级的线程安全日志记录器。

public static class SimpleLogger
{
private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private static string _logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
public static void Log(Exception ex)
{
try
{
if (!Directory.Exists(_lockDir)) Directory.CreateDirectory(_logDir);
string fileName = Path.Combine(_logDir, $"{DateTime.Now:yyyyMMdd}.log");
string content = $"[{DateTime.Now:HH:mm:ss}] {ex.Message}\n{ex.StackTrace}\n" +
new string('-', 30) + "\n";
_lock.EnterWriteLock();
File.AppendAllText(fileName, content);
}
finally
{
if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
}
}
}

四、 总结与最佳实践#

  1. 异常捕获不仅仅是记录:对于 DispatcherUnhandledException,如果异常不影响后续运行,设置 e.Handled = true 可以拦截崩溃。
  2. 不要在 HandleException 中抛出新异常:这会导致死循环。
  3. 配置文件属性:确保 log4net.config 的属性设置为 “如果较新则复制”,否则程序运行时找不到配置文件。
  4. 异步注意点async void 方法中的异常无法被全局捕获,应尽量使用 async Task
实战:构建稳健的全局异常捕获与日志监控系统
https://sw.rscclub.website/posts/wpfhandlerlog/
作者
杨月昌
发布于
2019-04-18
许可协议
CC BY-NC-SA 4.0