zoukankan      html  css  js  c++  java
  • ASP.NET Core 中间件自定义全局异常处理

    目录

    • 背景
    • ASP.NET Core过滤器(Filter)
    • ASP.NET Core 中间件(Middleware)
    • 自定义全局异常处理
      • .Net Core中使用ExceptionFilter
      • .Net Core中使用中间件
    • 总结
    • 参考

    背景

      作为开发者,你兴高采烈地完成了新系统的功能开发。并且顺利经过验收,系统如期上线,皆大欢喜。

      但是,有些bug就是在生产环境如期而至了。半夜梦酣之时,你被运维童鞋的电话惊醒了,系统不能正常运行了。接下来,他打包了一堆日志文件给你...

      笔者有幸做过几年运维自动化系统,深知产品的每一次大跌代上线都是一场很多IT人的噩梦。更甚者,开发和运维人员有时候因为定位一个线上问题,花了一个通宵或者甚至版本回退。

      干了多年开发越来越觉得,异常处理和定位的能力反映出开发者硬核能力。如果开发人员能够在对系统中异常进行捕获,然后记录日志,并对日志进行划分等级,然后通过邮件或者短信等提醒,是不是能够做到提前预判呢。

      
    在 asp.net core中全局异常处理,这里介绍两种不同的处理方式:过滤器捕获和中间件过滤。

    过滤器

      通过使用 ASP.NET Core 中的筛选器,可在请求处理管道中的特定阶段之前或之后运行代码。

      内置过滤器处理任务,例如:

    • 授权(防止用户访问未获授权的资源)。
    • 响应缓存(对请求管道进行短路出路,以便返回缓存的响应)。

      可以创建自定义过滤器,用于处理横切关注点。 横切关注点的示例包括错误处理、缓存、配置、授权和日志记录。 过滤器可以避免复制代码。 例如,错误处理异常过滤器可以合并错误处理。

    过滤器的工作原理

      过滤器在 ASP.NET Core 操作调用管道(有时称过滤器管道)内运行。 过滤器管道在 ASP.NET Core 选择了要执行的操作之后运行。

    使用场景

    过滤器类型

      熟悉.NET MVC框架的同学应该知道,MVC也提供了5大过滤器供我们用来处理请求前后需要执行的代码。分别是授权过滤器(AuthenticationFilter),资源过滤器(resource-filters),操作过滤器(ActionFilter),异常过滤器(ExceptionFilter),结果过滤器(ResultFilter)。

      每种过滤选器类型都过滤器管道中的不同阶段执行:

    • 授权过滤器最先运行,用于确定是否已针对请求为用户授权。 如果请求未获授权,授权过滤器可以让管道短路。

    • 资源过滤器

      • 授权后运行。
      • OnResourceExecuting 在过滤器管道的其余阶段之前运行代码。 例如,OnResourceExecuting 在模型绑定之前运行代码。
      • OnResourceExecuted 在管道的其余阶段完成之后运行代码。
    • 操作过滤器

      • 在调用操作方法之前和之后立即运行代码。
      • 可以更改传递到操作中的参数。
      • 可以更改从操作返回的结果。
      • 不可在 Razor Pages 中使用。
    • 异常过滤器在向响应正文写入任何内容之前,对未经处理的异常应用全局策略。
      结果过滤器在执行操作结果之前和之后立即运行代码。 仅当操作方法成功执行时,它们才会运行。 对于必须围绕视图或格式化程序的执行的逻辑,它们很有用。

      下图展示过滤器类型在筛选器管道中的交互方式。
    使用场景

    过滤器使用

      在.net core 中,一般是在StartUp.cs的ConfigureServices方法中注册

    // 将异常过滤器注入到容器中
    services.AddScoped<GlobalExceptionFilter>();
    

    中间件

    中间件(Middleware)的作用

      ASP.NETCore应用基于一系列中间件构建。中间件是排列到管道中的处理程序,用于处理请求和响应。 在 Web 窗体应用程序中,HTTP 处理程序和模块解决了类似的问题。 在 ASP.NET Core 中,模块、处理程序、 Global.asax.cs和应用程序生命周期替换为中间件。

      ASP.NET Core 请求管道包含一系列请求委托,依次调用。 下图演示了这一概念。 沿黑色箭头执行。

    使用场景

      可以看到,每一个中间件都都可以在请求之前和之后进行操作。请求处理完成之后传递给下一个请求。

    中间件的运行方式

      默认情况下,中间件的执行顺序根据Startup.cs中,Configure方法中注册的先后顺序执行。一般通过aApp.UseMiddleware<>()的方式注册中间件。

    // ExceptionMiddleware 加入管道
    app.UseMiddleware<ExceptionMiddleware>();
    

    使用ExceptionFilter

      前面提到,过滤器可以处理错误异常。这里可以实践一把。

      新建一个.NET Core MVC控制器(.net WebAPI也类似)。
      我在Test/Index Action方法中故意制造一个异常(我们知道在被除数不能为0).

    public IActionResult Index()
    {
         int a = 0, b = 5;
         var result = b/a;
    }
    

      在Visual Studio中调试报错了
    使用场景

      我们深知,异常这样报错很不友好,于是我们用了万能的try-catch

    public IActionResult Index()
    {
          try
            {
                int a = 0, b = 5;
                var result = b / a;
            } catch (Exception)
            {
                throw new ArgumentException("被除数不能为0", "a");
            }
    }
    

      这样异常提示确实友好了,并且我们拦截了异常,甚至可以将异常记录到日志中。
    使用场景

      但是每个方法都这样加会不会觉得很烦?有没有想过一劳永逸的办法。从架构层面应该这样思考。

      在传统的 Asp.Net MVC 应用程序中,我们一般都使用服务过滤的方式去捕获和处理异常,这种方式 非常常见,而且可用性来说,体验也不错,幸运的是 Asp.Net Core也完整的支持该方式。 新建一个全局异常过滤器GlobalExceptionFilter.cs,继承自IExceptionFilter。

    public class GlobalExceptionFilter:Attribute, IExceptionFilter
        {
            private readonly IHostingEnvironment _hostingEnvironment;
            private readonly IModelMetadataProvider _modelMetadataProvider;
    
            public GlobalExceptionFilter(
                IHostingEnvironment hostingEnvironment,
                IModelMetadataProvider modelMetadataProvider)
            {
                _hostingEnvironment = hostingEnvironment;
                _modelMetadataProvider = modelMetadataProvider;
            }
            /// <summary>
            /// 发生异常进入
            /// </summary>
            /// <param name="context"></param>
            public async void OnException(ExceptionContext context)
            {
                ContentResult result = new ContentResult
                {
                    StatusCode = 500,
                    ContentType = "text/json;charset=utf-8;"
                };
    
                if (_hostingEnvironment.IsDevelopment())
                {
                    var json = new { message = context.Exception.Message };
                    result.Content = JsonConvert.SerializeObject(json);
                }
                else
                {
                    result.Content = "抱歉,出错了";
                }
                context.Result = result;
                context.ExceptionHandled = true;
            }
        }
    

      我们在startup.cs中进行中注入

    // 将异常过滤器注入到容器中
    services.AddScoped<GlobalExceptionFilter>();
    

      然后在需要的控制器上加上特性**ServiceFilter(typeof(GlobalExceptionFilter))]

     [ServiceFilter(typeof(GlobalExceptionFilter))]
     public class TestController : Controller
    

      启动程序,错误提示,页面只会显示单纯错误信息。

    {"message":"Attempted to divide by zero."}

    Net Core中使用中间件方式

      首先,创建一个中间件ExceptionMiddleware

    public class ExceptionMiddleware
        {
            private readonly RequestDelegate next;
            private IHostingEnvironment environment;
    
            public ExceptionMiddleware(RequestDelegate next,IHostingEnvironment environment)
            {
                this.next = next;
                this.environment = environment;
            }
    
            public async Task Invoke(HttpContext context)
            {
                try
                {
                    await next.Invoke(context);
                    var features = context.Features;
                }
                catch (Exception e)
                {
                    await HandleException(context, e);
                }
            }
    
            private async Task HandleException(HttpContext context, Exception e)
            {
                context.Response.StatusCode = 500;
                context.Response.ContentType = "text/json;charset=utf-8;";
                string error = "";
    
                if (environment.IsDevelopment())
                {
                    var json = new { message = e.Message};
                    error = JsonConvert.SerializeObject(json);
                }
                else
                    error = "抱歉,出错了";
    
                await context.Response.WriteAsync(error);
            }
        }
    

      创建 HandleException(HttpContext context, Exception e) 处理异常,判断是 Development 环境下,输出详细的错误信息,非 Development 环境仅提示调用者“抱歉,出错了”,同时使用 NLog 组件将日志写入硬盘;同样,在 Startup.cs 中将 ExceptionMiddleware 加入管道中

    //ExceptionMiddleware 加入管道
    app.UseMiddleware<ExceptionMiddleware>();
    

    启动调试,结果如下

    {"message":"Attempted to divide by zero."}
    

      统一封装冷异常处理方式和消息格式,对前端也很友好。

    总结

      通过依赖注入和管道中间件两种不同的全局捕获异常处理。实际项目中,也是应当区分不同的业务场景,输出不同的日志信息,不管是从安全或者是用户体验友好性上面来说,都是非常值得推荐的方式,全局异常捕获处理,完全和业务剥离。

      从运维的角度看,将异常处理的日志进行统一采集和分类,便于接入ELK,或者第三方日志系统。方便检测日志,从而监测系统健康状况。

    参考

    使用场景

  • 相关阅读:
    论文复现
    20199324 2019-2020-2 《网络攻防实践》第12周作业
    20199324 2019-2020-2 《网络攻防实践》第10周作业
    20199324 2019-2020-2 《网络攻防实践》第9周作业
    Android Dalvikvm的学习笔记
    20199324 2019-2020-2 《网络攻防实践》第8周作业
    20199324 2019-2020-2 《网络攻防实践》第7周作业
    445. Add Two Numbers II
    167. Two Sum II
    643. Maximum Average Subarray I
  • 原文地址:https://www.cnblogs.com/lucky_hu/p/12444832.html
Copyright © 2011-2022 走看看