一、为什么需要异常处理中间件
在开发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 典型应用场景
- Web API项目:统一返回格式化的错误响应
- MVC网站:显示友好的错误页面
- 后台服务:记录详细的错误日志便于排查
5.2 技术优缺点
优点:
- 统一处理异常,避免代码重复
- 可以集中记录日志
- 提供一致的用户体验
- 便于区分开发和生产环境的错误显示
缺点:
- 可能会隐藏一些应该立即修复的问题
- 过度捕获异常可能导致性能问题
- 需要仔细设计错误响应格式
5.3 注意事项
- 生产环境不要显示详细的错误信息,防止信息泄露
- 对于不同的HTTP方法(GET/POST等)可能需要不同的处理方式
- 注意中间件的顺序,异常处理中间件应该尽可能早地添加到管道中
- 对于特别关键的操作,可能需要单独处理异常而不是全局捕获
六、总结
异常处理是Web开发中不可忽视的重要环节。通过ASP.NET Core的中间件管道,我们可以灵活地实现各种异常处理策略。无论是简单的异常捕获,还是复杂的错误分类处理,中间件都能很好地胜任。
记住,好的异常处理应该:
- 记录足够的信息用于排查问题
- 给用户友好的反馈
- 区分不同类型的错误
- 考虑安全性问题
通过本文介绍的技术,你应该能够构建一个健壮的异常处理系统,让你的应用更加稳定可靠。
评论