一、为什么需要异常处理中间件

在开发Web应用时,错误和异常是不可避免的。想象一下,当用户访问你的网站时突然出现一个未处理的异常,用户看到的是难懂的技术错误信息,这体验多糟糕啊。ASP.NET Core的中间件管道给了我们一个很好的机会来统一处理这些异常。

中间件就像是一个流水线,每个请求都会经过这个流水线。我们可以在管道中插入一个专门处理异常的中间件,这样无论应用哪个环节出了问题,都能被统一捕获和处理。

二、基础异常处理中间件实现

让我们从一个最简单的异常处理中间件开始。这里我们使用ASP.NET Core 6.0技术栈。

// ASP.NET Core 6.0示例
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // 继续执行管道中的下一个中间件
            await _next(context);
        }
        catch (Exception ex)
        {
            // 记录异常日志
            _logger.LogError(ex, "发生未处理异常");
            
            // 设置响应状态码
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            
            // 返回友好的错误信息
            await context.Response.WriteAsync("抱歉,服务器开小差了,请稍后再试");
        }
    }
}

这个中间件的工作原理很简单:它尝试执行下一个中间件,如果出现异常就捕获它,记录日志,然后返回一个友好的错误信息给用户。

要在应用中使用它,只需要在Program.cs中添加:

// 在Program.cs中添加中间件
app.UseMiddleware<ExceptionHandlingMiddleware>();

三、进阶异常处理策略

基础的异常处理虽然能用,但还不够完善。让我们看看如何改进它。

3.1 区分不同类型的异常

不同的异常可能需要不同的处理方式。比如,业务逻辑异常和系统异常应该区别对待。

public async Task InvokeAsync(HttpContext context)
{
    try
    {
        await _next(context);
    }
    catch (BusinessException ex)
    {
        // 业务异常,返回400状态码和具体错误信息
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
        await context.Response.WriteAsJsonAsync(new
        {
            Code = ex.Code,
            Message = ex.Message
        });
    }
    catch (UnauthorizedAccessException)
    {
        // 权限异常,返回401
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        await context.Response.WriteAsync("请先登录");
    }
    catch (Exception ex)
    {
        // 其他未预料异常
        _logger.LogError(ex, "系统异常");
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await context.Response.WriteAsync("系统繁忙,请稍后再试");
    }
}

3.2 使用内置异常处理中间件

ASP.NET Core其实已经提供了内置的异常处理中间件,我们可以直接使用:

// 配置异常处理页面
if (app.Environment.IsDevelopment())
{
    // 开发环境显示详细错误
    app.UseDeveloperExceptionPage();
}
else
{
    // 生产环境使用自定义错误页
    app.UseExceptionHandler("/error");
}

// 然后添加一个处理/error的路由
app.Map("/error", handler =>
{
    handler.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await context.Response.WriteAsync("发生错误,请联系管理员");
    });
});

四、全局错误捕获的最佳实践

4.1 结合日志记录

良好的日志记录对于排查问题至关重要。我们可以使用ILogger接口记录详细的异常信息:

catch (Exception ex)
{
    _logger.LogError(ex, "请求处理失败: {Path}", context.Request.Path);
    
    // 获取异常堆栈中最内层的异常
    var innerEx = ex;
    while (innerEx.InnerException != null)
    {
        innerEx = innerEx.InnerException;
        _logger.LogError(innerEx, "内部异常");
    }
    
    // 其他处理逻辑...
}

4.2 自定义异常处理过滤器

除了中间件,我们还可以使用异常过滤器来处理控制器中的异常:

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "控制器异常");
        
        context.Result = new JsonResult(new
        {
            Success = false,
            Message = "处理请求时发生错误"
        })
        {
            StatusCode = StatusCodes.Status500InternalServerError
        };
        
        context.ExceptionHandled = true;
    }
}

在Program.cs中注册:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});

五、应用场景与注意事项

5.1 典型应用场景

  1. Web API项目:统一返回格式化的错误响应
  2. MVC网站:显示友好的错误页面
  3. 后台服务:记录详细的错误日志便于排查

5.2 技术优缺点

优点:

  • 统一处理异常,避免代码重复
  • 可以集中记录日志
  • 提供一致的用户体验
  • 便于区分开发和生产环境的错误显示

缺点:

  • 可能会隐藏一些应该立即修复的问题
  • 过度捕获异常可能导致性能问题
  • 需要仔细设计错误响应格式

5.3 注意事项

  1. 生产环境不要显示详细的错误信息,防止信息泄露
  2. 对于不同的HTTP方法(GET/POST等)可能需要不同的处理方式
  3. 注意中间件的顺序,异常处理中间件应该尽可能早地添加到管道中
  4. 对于特别关键的操作,可能需要单独处理异常而不是全局捕获

六、总结

异常处理是Web开发中不可忽视的重要环节。通过ASP.NET Core的中间件管道,我们可以灵活地实现各种异常处理策略。无论是简单的异常捕获,还是复杂的错误分类处理,中间件都能很好地胜任。

记住,好的异常处理应该:

  • 记录足够的信息用于排查问题
  • 给用户友好的反馈
  • 区分不同类型的错误
  • 考虑安全性问题

通过本文介绍的技术,你应该能够构建一个健壮的异常处理系统,让你的应用更加稳定可靠。