异常处理技巧
- 用特定的异常类或接口表示业务处理异常
- 为业务逻辑异常定义全局错误码
- 为未知异常定义特点的输出信息和错误码
- 对于已知业务逻辑异常响应 HTTP 200(监控系统友好)
- 对于未预见的异常响应 HTTP 500
- 为所有的异常记录详细的日志
错误处理页:
Asp.Net Core 在开发情况下为我们启用了错误处理中间件。
但在生产模式中我们应该关闭掉,我们正常处理我们错误的方式:
- 自定义错误页
准备工作:
定义一个IknowException接口:
public interface IKnownException { public string Message { get; } public int ErrorCode { get; } public object[] ErrorData { get; } }
定义一个KnowException类并实现IknowException接口:
public class KnownException : IKnownException { public string Message { get; private set; } public int ErrorCode { get; private set; } public object[] ErrorData { get; private set; } public static readonly IKnowException UnKnownException = new KnowException { Message = "未知的错误", ErrorCode = 9999}; public static IKnownException FromKnownException(IKnowException exception) { return new KnownException {Message = exception.Message, ErrorCode = exception.ErrorCode, ErrorData = exception.ErrorData}; } }
我们为什么要定义这样一个类型,是因为通常情况下,系统发生的异常和业务逻辑的异常是不同的。
系统发生的异常可能为网络出现异常,数据库连接出现异常,Redis的连接出现了异常等等。
比如说业务逻辑异常为输入的参数或订单的状态不符合条件,当前余额不足这种信息,我们的处理方式为对不同的逻辑输出不同的业务对象,或输出一个异常,用异常来承载我们的逻辑的特殊分支。所以我们需要识别哪些是系统异常哪些是业务逻辑异常。
ErrorController:
public class ErrorController : Controller { [Route("/error")] public IActionResult Index() { var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>(); var ex = exceptionHandlerPathFeature?.Error; if (!(ex is IKnowException knowException)) { var logger = HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>(); if (ex != null) logger.LogError(ex, ex.Message); knowException = KnowException.UnKnowException; } else { knowException = KnowException.FromKnowException(knowException); } return View(knowException); } }
对于否为未知的异常,我们不应该把我们错误的异常完整的输出给客户端,应该传递一个特殊的错误信息(在此我们传递回错误信息为未知的错误,错误号为9999的一个错误信息)。
但在日志系统中我们应记录完整的错误信息。
- 使用代理方法
在StartUp类中使用中间件委托代理处理异常
app.UseExceptionHandler(errApp => { errApp.Run(async context => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var ex = exceptionHandlerPathFeature?.Error; if (!(ex is IKnownException knowException)) { var logger = context.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>(); if (ex != null) logger.LogError(ex, ex.Message); knowException = KnownException.UnKnowException; context.Response.StatusCode = StatusCodes.Status500InternalServerError; } else { knowException = KnownException.FromKnowException(knowException); context.Response.StatusCode = StatusCodes.Status200OK; } var jsonOptions = context.RequestServices.GetService<IOptions<JsonOptions>>(); context.Response.ContentType = "application/json; charset=utf-8"; await context.Response.WriteAsync( System.Text.Json.JsonSerializer.Serialize(knowException, jsonOptions.Value.JsonSerializerOptions)); }); });
这里对于未知的系统异常我们应该输出 HTTP 500, 而对于业务逻辑的异常输出 HTTP 200。是因为监控系统会对这些错误进行识别,当识别到响应为500的比例较高的情况下,我们认为系统的可用性出现问题。对已知的业务逻辑错误使用200处理是正常的行为。这样使得告警系统更加灵敏,防止业务逻辑的异常去干扰告警系统。
上述中间件可捕捉我们自定义的异常:
public class MyServerException : Exception, IKnownException { public MyServerException(string message, int errorCode, params object[] errorData) : base(message) { this.ErrorCode = errorCode; this.ErrorData = errorData; } public int ErrorCode { get; private set; } public object[] ErrorData { get; private set; } }
抛出异常:
浏览器响应:
- 异常过滤器(作用在MVC体系之下而不是中间件)
准备工作: 定义自己的异常过滤器,需要实现IExceptionFilter接口。
public class MyExceptionFilter : IExceptionFilter { public void OnException(ExceptionContext context) { IKnownException knownException = context.Exception as IKnownException; if (knownException == null) { var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>(); logger.LogError(context.Exception, context.Exception.Message); knownException = KnownException.UnKnowException; context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; } else { knownException = KnownException.FromKnowException(knownException); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; } context.Result = new JsonResult(knownException) { ContentType = "application/json; charset=utf-8" }; } }
使用场景一般是对于控制器的特殊异常处理,或当中间件有一套处理异常方式时,需要另外一个异常处理方式时也可用此方式。使用时在ConfigureServices里为MVC模式添加Filter即可。
services.AddMvc(options => { options.Filters.Add<MyExceptionFilter>(); }).AddJsonOptions(options => { options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default; });
对于MVC还有一种使用Attribute的方式:需实现ExceptionFilterAttribute接口
public class MyExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { IKnownException knownException = context.Exception as IKnownException; if (knownException == null) { var logger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>(); logger.LogError(context.Exception, context.Exception.Message); knownException = KnownException.UnKnowException; context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; } else { knownException = KnownException.FromKnowException(knownException); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; } context.Result = new JsonResult(knownException) { ContentType = "application/json; charset=utf-8" }; } }
这样可以将Attribute应用到你想进行异常处理的控制器即可。