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

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

一、 为什么选择 log4net?#

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

  • 分级记录:支持 DEBUG, INFO, WARN, ERROR, FATAL 多个级别。
  • 动态配置:无需重新编译,通过修改 XML 即可改变日志输出逻辑。
  • 性能优异:内部通过异步缓冲机制,对 UI 线程的阻塞极小。

二、 全局异常捕获架构#

在 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 的速度将因详细的日志记录而提升数倍。

WPF 实战:构建企业级全局异常捕获与 log4net 日志监控系统
https://sw.rscclub.website/posts/wpflog4neterror/
作者
杨月昌
发布于
2016-11-19
许可协议
CC BY-NC-SA 4.0