区分真异常和逻辑异常

异常处理技巧

  • 用特定的异常类或接口表示业务处理异常
  • 为业务逻辑异常定义全局错误码
  • 为未知异常定义特点的输出信息和错误码
  • 对于已知业务逻辑异常响应 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应用到你想进行异常处理的控制器即可。

原文地址:https://www.cnblogs.com/Xieyiincuit/p/13997417.html