.NET Core 开发实战
  • 目录
  • 第1课:课程介绍
  • 第2课:内容综述
  • 第3课:.NET Core的现状、未来以及环境搭建
  • 第4课:Startup:掌握ASP.NET Core的启动过程
  • 第5课:依赖注入:良好架构的起点
  • 第6课:作用域与对象释放行为
  • 第7课:用Autofac增强容器能力
  • 第8课:配置框架:让服务无缝适应各种环境
  • 第9课:命令行配置提供程序
  • 第10课:环境变量配置提供程序
  • 第11课:文件配置提供程序
  • 第12课:配置变更监听
  • 第13课:配置绑定:使用强类型对象承载配置数据
  • 第14课:自定义配置数据源:低成本实现定制化配置方案
  • 第15课:选项框架:服务组件集成配置的最佳实践
  • 第16课:选项数据热更新:让服务感知配置的变化
  • 第17课:为选项数据添加验证:避免错误配置的应用接收用户流量
  • 第18课:日志框架:聊聊记日志的最佳姿势
  • 第19课:日志作用域:解决不同请求之间的日志干扰
  • 第20课:结构化日志组件Serilog:记录对查询分析友好的日志
  • 第21课:中间件:掌控请求处理过程的关键
  • 第22课:异常处理中间件:区分真异常与逻辑异常
  • 第23课:静态文件中间件:前后端分离开发合并部署骚操作
  • 第24课:文件提供程序:让你可以将文件放在任何地方
  • 第25课:路由与终结点:如何规划好你的Web API
  • 第26课:工程结构概览:定义应用分层及依赖关系
  • 第27课:定义Entity:区分领域模型的内在逻辑和外在行为
  • 第28课:工作单元模式(UnitOfWork):管理好你的事务
  • 第29课:定义仓储:使用EF Core实现仓储层
  • 第30课:领域事件:提升业务内聚,实现模块解耦
  • 第31课:APIController:定义API的最佳实践
  • 第32课:集成事件:解决跨微服务的最终一致性
  • 第33课:集成事件:使用RabbitMQ来实现EventBus
  • 第34课:MediatR:轻松实现命令查询职责分离模式(CQRS)
  • 第35课:MediatR:让领域事件处理更加优雅
Powered by GitBook
On this page

第22课:异常处理中间件:区分真异常与逻辑异常

学习分享 丨作者 / 郑 子 铭 丨公众号 / DotNet NB / CloudNative NB

Previous第21课:中间件:掌控请求处理过程的关键Next第23课:静态文件中间件:前后端分离开发合并部署骚操作

Last updated 3 years ago

这一节我们来讲解一下错误处理的最佳实践

系统里面异常处理,ASP.NET Core 提供了四种方式

1、异常处理页

2、异常处理匿名委托方法

3、IExceptionFilter

4、ExceptionFilterAttribute

Startup 的 Configure 方法

if (env.IsDevelopment())
{
    // 开发环境下的异常处理页
    app.UseDeveloperExceptionPage();
}

控制器抛出异常

throw new Exception("报个错");

启动程序,可以看到一个错误页

这个错误页会输出我们当前请求的详细信息和错误的详细信息,这种页面是不适合给用户看到的,所以这样的错误页在生产环境是需要关闭的

以下是正常处理错误页的方式:

// 第一种方式就是定义错误页的方式
app.UseExceptionHandler("/error");

定义一个接口 IKnownException

namespace ExceptionDemo.Exceptions
{
    public interface IKnownException
    {
        public string Message { get; }

        public int ErrorCode { get; }

        public object[] ErrorData { get; }
    }
}

默认实现 KnownException

namespace ExceptionDemo.Exceptions
{
    public class KnownException : IKnownException
    {
        public string Message { get; private set; }

        public int ErrorCode { get; private set; }

        public object[] ErrorData { get; private set; }

        public readonly static IKnownException Unknown = new KnownException { Message = "未知错误", ErrorCode = 9999 };

        public static IKnownException FromKnownException(IKnownException exception)
        {
            return new KnownException { Message = exception.Message, ErrorCode = exception.ErrorCode, ErrorData = exception.ErrorData };
        }
    }
}

为什么需要定义这样一个类型呢?

因为通常情况下我们系统里面的异常和我们业务逻辑的异常是不同的,业务逻辑上面的判断异常,比如说输入的参数,订单的状态不符合条件,当前账户余额不足,这样子的信息我们有两种处理方式:

一种处理方式就是对不同的逻辑输出不同的业务对象

还有一种方式就是对于异常的这种业务逻辑,输出一个异常,用异常来承载逻辑的特殊分支,这个时候就需要识别出来哪些是业务的异常,哪些是不确定的未知的异常,比如说网络的请求出现了异常,MySql 的连接闪断了,Redis 的连接出现了异常

接着通过定义一个错误页来承载错误信息,比如我们的 ErrorController,它只有一个页面,它的作用就是输出错误信息

namespace ExceptionDemo.Controllers
{
    [AllowAnonymous]
    public class ErrorController : Controller
    {
        [Route("/error")]
        public IActionResult Index()
        {
            // 获取当前上下文里面报出的异常信息
            var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

            var ex = exceptionHandlerPathFeature?.Error;

            // 特殊处理,尝试转换为 IKnownException
            var knownException = ex as IKnownException;
            // 对于未知异常,我们并不应该把错误异常完整地输出给客户端,而是应该定义一个特殊的信息 Unknown 传递给用户
            // Unknown 其实也是一个 IKnownException 的实现,它的 Message = "未知错误", ErrorCode = 9999
            // 也就是说我们在控制器 throw new Exception("报个错"); 就会看到错误信息
            if (knownException == null)
            {
                knownException = KnownException.Unknown;
            }
            else// 当识别到异常是已知的业务异常时,输出已知的异常,包括异常消息,错误状态码和错误信息,就是在 IKnownException 中的定义
            {
                knownException = KnownException.FromKnownException(knownException);
            }
            return View(knownException);
        }
    }
}

View

@model ExceptionDemo.Exceptions.IKnownException
@{
    ViewData["Title"] = "Index";
}

<h1>错误信息</h1>

<div>Message:<label>@Model.Message</label></div>
<div>ErrorCode<label>@Model.ErrorCode</label></div>

在 ConfigureServices 中添加 mvc

public void ConfigureServices(IServiceCollection services)
{
      services.AddControllers();

      services.AddMvc();
}

启动程序之后可以看到自定义的错误页已经成功渲染出来了

这就是第一种处理错误的方式

接下来介绍使用代理方法的方式,也就是说把 ErrorController 整段逻辑直接定义在注册的地方,使用一个匿名委托来处理,这里的逻辑与之前的逻辑是相同的

app.UseExceptionHandler(errApp =>
{
    errApp.Run(async context =>
    {
        // 在 Features 里面获取异常
        var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
        // 识别异常是否为 IKnownException
        IKnownException knownException = exceptionHandlerPathFeature.Error as IKnownException;
        if (knownException == null)
        {
            // 如果不是则记录并且把错误的响应码响应成 Http 500
            var logger = context.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();
            logger.LogError(exceptionHandlerPathFeature.Error, exceptionHandlerPathFeature.Error.Message);
            knownException = KnownException.Unknown;
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        }
        else
        {
            // 如果捕获到的是一个业务逻辑的异常,Http 响应码应该给是 200
            knownException = KnownException.FromKnownException(knownException);
            context.Response.StatusCode = StatusCodes.Status200OK;
        }
        // 然后再把响应信息通过 json 的方式输出出去
        var jsonOptions = context.RequestServices.GetService<IOptions<JsonOptions>>();
        context.Response.ContentType = "application/json; charset=utf-8";
        await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(knownException, jsonOptions.Value.JsonSerializerOptions));
    });
});

为什么对于未知的异常要输出 Http 500,而对于业务逻辑的异常,建议输出 Http 200?

因为监控系统实际上会对 Http 的响应码进行识别,当监控系统识别到 Http 响应是 500 的比例比较高的情况下,会认为系统的可用性有问题,这个时候告警系统就会发出警告

对于已知的业务逻辑的这种正常的识别的话,用正常的 Http 200 来处理是一个正常的行为,这样就可以让监控系统更好的工作,正确的识别出系统的一些未知的错误信息,错误的告警,让告警系统更加的灵敏,也避免了业务逻辑的异常干扰告警系统

接下来看一下第三种,通过异常过滤器的方式

这种方式实际上是作用在 MVC 的整个框架的体系下面的,它并不是在中间件的最早期发生作用的,它是在 MVC 的整个生命周期里面发生作用,也就是说它只能工作在 MVC Web API 的请求周期里面

首先自定义一个 MyExceptionFilter

namespace ExceptionDemo.Exceptions
{
    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.Unknown;
                context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException = KnownException.FromKnownException(knownException);
                context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
            }
            context.Result = new JsonResult(knownException)
            {
                ContentType = "application/json; charset=utf-8"
            };
        }
    }
}

处理逻辑与之前的相同

接着注册 Filters

services.AddMvc(mvcOptions =>
{
    mvcOptions.Filters.Add<MyExceptionFilter>();
}).AddJsonOptions(jsonoptions =>
{
    jsonoptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});

启动程序,输出如下:

{"message":"未知错误","errorCode":9999,"errorData":null}

输出与之前的一致,因为这是在 Controller 里面输出了错误

如果在 MVC 的中间件之前输出错误的话,它是没办法处理的

这个场景一般情况下是指需要对 Controller 进行特殊的异常处理,而对于中间件整体来讲的话,又要用另一种特殊的逻辑来处理的时候,可以用 ExceptionFilter 的方式处理

这种方式还可以通过 Attribute 的方式

自定义一个 MyExceptionFilterAttribute

namespace ExceptionDemo.Exceptions
{
    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.Unknown;
                context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException = KnownException.FromKnownException(knownException);
                context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
            }
            context.Result = new JsonResult(knownException)
            {
                ContentType = "application/json; charset=utf-8"
            };
        }
    }
}

在 Controller 上面标注 MyExceptionFilter

[MyExceptionFilter]
public class WeatherForecastController : ControllerBase

启动运行之后效果相同

这两种方式的效果是对等的,区别在于说可以更细粒度的对异常处理进行控制,可以指定部分的 Controller 或者 Exception,来决定我们的异常处理,也可以在全局注册 ExceptionFilter

当然因为 ExceptionFilterAttribute 也实现了 IExceptionFilter,所以它也可以注册到全局,也可以把它当作全局异常处理的过滤器来使用,Controller 上面也就不需要标记了

注册 Filters

services.AddMvc(mvcOptions =>
{
    //mvcOptions.Filters.Add<MyExceptionFilter>();
    mvcOptions.Filters.Add<MyExceptionFilterAttribute>();
}).AddJsonOptions(jsonoptions =>
{
    jsonoptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});

在 Controller 上面取消标注 MyExceptionFilter

//[MyExceptionFilter]
public class WeatherForecastController : ControllerBase

启动程序,输出结果一致

这个场景对于我们定义一些 API,然后对 API 进行定义我们的异常处理的约定是很有帮助的

总结一下

首先我们需要定义特定的异常类或者接口,我们可以定义抽象类,也可以用接口的方式,例子中是通过接口的方式表示业务逻辑的异常

对于业务逻辑的异常,实际上需要定义全局的错误码

对于未知的异常,应该输出特定的输出信息和错误码,然后记录完整的日志,我们不应该把系统内部的一些比如说异常堆栈这些信息输出给用户

对于已知的业务逻辑的异常,用 Http 200 的方式,对于未知的异常,用 Http 500 的方式,这样可以让监控系统更好的工作

另外一个建议就是尽量记录所有的异常的详细信息,以供后续对日志进行分析,也供监控系统做一些特定的监控警告

GitHub源码链接:

https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/ExceptionDemo