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

WPF 实战:构建企业级全局异常捕获与 log4net 日志监控系统

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

一、 为什么选择 log4net?

在 .NET 生态中,log4net 凭借其成熟、稳定和极高的配置灵活性,依然是许多企业级项目的首选方案:


二、 全局异常捕获架构

在 WPF 中,异常可能来源于三个维度,必须针对性地进行“围堵”:

2.1 核心实现方案

App.xaml.cs 中注册全局事件,这是程序的“最后一道防线”。

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        
        // 1. 初始化日志系统
        ZLogHelper.InitLog4Net();

        // 2. 注册全维度异常监听
        RegisterEvents();
    }

    private void RegisterEvents()
    {
        // [维度1] UI主线程未捕获异常:通常是由按钮点击、界面渲染等触发
        this.DispatcherUnhandledException += App_DispatcherUnhandledException;

        // [维度2] 非UI线程未捕获异常:如子线程 Thread 抛出的错误
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

        // [维度3] Task任务异常:处理 async/await 且未被观察到的异常
        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    }

    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 static void HandleException(Exception ex)
    {
        if (ex == null) return;

        // 记录错误日志
        ZLogHelper.Logerror.Error("系统捕获到未处理的异常:", ex);

        // 弹出友好提示
        Current.Dispatcher.Invoke(() =>
        {
            MessageBox.Show($"程序遇到意外错误:{ex.Message}\n详情请查看日志文件。", 
                            "系统异常", MessageBoxButton.OK, MessageBoxImage.Error);
        });
    }
}

三、 log4net 配置深度解析

在项目的 log4net.config 中,我们可以使用 RollingFileAppender 来实现自动分片存储。

提示:请确保该配置文件的属性设置为 “如果较新则复制” 到输出目录。

<?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'.htm'" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="&lt;HR COLOR=red&gt;%n【异常时间】:%d [%t] &lt;BR&gt;%n【日志级别】:%-5p &lt;BR&gt;%n【堆栈信息】:%c &lt;BR&gt;%n【详情】:%m &lt;BR&gt;%n" />
    </layout>
    <filter type="log4net.Filter.LevelRangeFilter">
      <levelMin value="ERROR" />
      <levelMax value="FATAL" />
    </filter>
  </appender>

  <logger name="logerror">
    <level value="ALL" />
    <appender-ref ref="ErrorAppender" />
  </logger>
</log4net>

四、 退出时的资源回收

OnExit 中,除了关闭日志,还可以清理运行产生的空文件,保持项目目录整洁。

protected override void OnExit(ExitEventArgs e)
{
    ZLogHelper.StopLog(); // 确保缓冲区内容全部写入磁盘
    CleanupEmptyLogFiles();
    base.OnExit(e);
}

private void CleanupEmptyLogFiles()
{
    try
    {
        string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
        if (!Directory.Exists(path)) return;

        var files = Directory.GetFiles(path, "*.htm", SearchOption.AllDirectories);
        foreach (var file in files)
        {
            var info = new FileInfo(file);
            if (info.Length == 0) info.Delete(); // 删除没有任何记录的空日志
        }
    }
    catch { /* 忽略清理过程中的异常 */ }
}

五、 总结与最佳实践

  1. 异常拦截优先级:对于 DispatcherUnhandledException,如果异常不影响后续运行,设置 e.Handled = true 可以保住程序进程。
  2. 避免捕获器崩溃:在 HandleException 内部的代码务必极度稳健,严禁再次抛出未捕获异常。
  3. 异步注意点async void 方法中的异常无法被全局捕获,应尽量使用 async Task

通过这套方案,你可以将 WPF 应用从“一碰就碎”转化为“坚韧稳健”。你会发现,解决生产环境 Bug 的速度将因详细的日志记录而提升数倍。



上一篇
深度直击:后向传播算法(Backpropagation)全解析
下一篇
WPF 实战:将 DataGrid 数据导出为 XML 文件