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(); } }}四、 总结与最佳实践
- 异常捕获不仅仅是记录:对于
DispatcherUnhandledException,如果异常不影响后续运行,设置e.Handled = true可以拦截崩溃。 - 不要在 HandleException 中抛出新异常:这会导致死循环。
- 配置文件属性:确保
log4net.config的属性设置为 “如果较新则复制”,否则程序运行时找不到配置文件。 - 异步注意点:
async void方法中的异常无法被全局捕获,应尽量使用async Task。
实战:构建稳健的全局异常捕获与日志监控系统
https://sw.rscclub.website/posts/wpfhandlerlog/