跳转到主要内容
杨月昌的软件博客
返回

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

在生产环境中,未经处理的异常会导致程序直接闪退(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


上一篇
AES 加密与解密:保障数据安全的利器
下一篇
手把手教你编译 OpenCV 4 + Contrib:VS2017 实战指南