NetCore 异常处理过滤器、中间件 、并整合Log4Net

十年河东,十年河西,莫欺少年穷

学无止境,精益求精

背景

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

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

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

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

过滤器

ASP.NET Core 有以下五种Filter 可以使用:

  • Authorization Filter:
    Authorization是五种Filter中优先级最高的,通常用于验证Request合不合法,不合法后面就直接跳过。
  • Resource Filter:Resource是第二优先,会在Authorization之后,Model Binding之前执行。通常会是需要对Model加工处理才用。
  • Exception Filter:异常处理的Filter。
  • Action Filter:最常使用的Filter,封包进出都会经过它,使用上没什么需要特别注意的。跟Resource Filter很类似,但并不会经过Model Binding。
  • Result Filter:当Action完成后,最终会经过的Filter。

 今天探讨异常过滤器、异常处理中间件、及NetCore结合 Log4Net 进行日志记录

使用ExceptionFilter

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

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

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

我们运行这个页面,如下:

 

 

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

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

代码如下(这里面结合了Log4Net ,用于一旦发生异常,则记录相关日志):

using log4net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NetCoreXXMS.NetCoreFilter
{
    public class GlobalExceptionFilter : Attribute, IExceptionFilter
    {
        private ILog log;
        private readonly IHostingEnvironment _hostingEnvironment;
        private readonly IModelMetadataProvider _modelMetadataProvider;

        public GlobalExceptionFilter(
            IHostingEnvironment hostingEnvironment,
            IModelMetadataProvider modelMetadataProvider)
        {
            this.log = LogManager.GetLogger(Startup.repository.Name, typeof(GlobalExceptionFilter));
            _hostingEnvironment = hostingEnvironment;
            _modelMetadataProvider = modelMetadataProvider;
        }
        /// <summary>
        /// 发生异常进入
        /// </summary>
        /// <param name="context"></param>
        public 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 };
                log.Error(json);
                result.Content = JsonConvert.SerializeObject(json);
            }
            else
            {
                result.Content = "抱歉,出错了";
            }
            context.Result = result;
            context.ExceptionHandled = true;
        }
    }
}

我们在startup.cs方法:ConfigureServices 中进行注册

services.AddSingleton<GlobalExceptionFilter>();

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

    [ServiceFilter(typeof(GlobalExceptionFilter))]
    public class MangerController : Controller
    {
        public IActionResult Index()
        {
           
            try
            {
                int a = 0, b = 5;
                var result = b / a;
            }
            catch (Exception)
            {
                throw new ArgumentException("被除数不能为0", "路径:Manger/index");
            }
            return View();
        }

      
    }

这样。我们运行项目,则会出现如下效果:

 同时,我们的日志也会记录,如下:

 这样,异常过滤器就完成了。

既然有了异常过滤器,那么我们是否还需要异常中间件呢?异常中间件和异常过滤器的区别和联系是什么呢?

1、首先,两者的功能类似,都是用于异常拦截处理

2、过滤器作用于具体的控制器,或者Action,过滤器关注具体的点 ,而中间件则作用于整个应用系统,这是两者作用域的范围差别。

3、使用过滤器,我们必须显式⑩引入,譬如上述代码中的:

[ServiceFilter(typeof(GlobalExceptionFilter))]
    public class MangerController : Controller
而中间件无需显式引入,它是基于AOP的切面拦截

Net Core中使用中间件方式

首先建一个中间件,如下:

using log4net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NetCoreXXMS.NetCoreFilter
{
    public class ExceptionMiddlewares
    {
        private ILog log;
        private readonly RequestDelegate next;
        private IHostingEnvironment environment;

        public ExceptionMiddlewares(RequestDelegate next, IHostingEnvironment environment)
        {
            this.log = LogManager.GetLogger(Startup.repository.Name, typeof(ExceptionMiddlewares));
            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 };
                log.Error(json);
                error = JsonConvert.SerializeObject(json);
            }
            else
                error = "抱歉,出错了";

            await context.Response.WriteAsync(error);
        }
    }
}

然后,在启动类Configure方法中注册该中间件

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            //注册异常中间件
            app.UseMiddleware<ExceptionMiddlewares>();
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
View Code

启动调试后,也会出现如下页面:

日志如下:

 

 控制器代码如下:

    public class MiddController : Controller
    {
        private ILog log;
        public MiddController()
        {
            this.log = LogManager.GetLogger(Startup.repository.Name, typeof(HomeController));
        }
        public IActionResult Index()
        {
            log.Error("测试日志");
            log.Info("测试日志");
            try
            {
                int a = 0, b = 5;
                var result = b / a;
            }
            catch (Exception)
            {
                throw new ArgumentException("被除数不能为0", "路径:Manger/index");
            }
            return View();
        }
    }
View Code

总之

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

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

因此,我们有必要引入Log4Net

Log4Net是一款优秀的日志插件

 1、在项目中引用Nuget程序包,Log4Net V2.0.8 最稳定版

2、在项目中增加Log4Net配置文件,并命名为:Log4Net.config,如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <!-- This section contains the log4net configuration settings -->
  <log4net>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <layout type="log4net.Layout.PatternLayout" value="%date [%thread] %-5level %logger - %message%newline" />
    </appender>

    <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
      <file value="Log\\LogInfo\\" />  
      <appendToFile value="true" />
      <rollingStyle value="Composite" />
      <staticLogFileName value="false" />
      <datePattern value="yyyyMMdd'.log'" />
      <maxSizeRollBackups value="10" />
      <maximumFileSize value="5MB" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%n异常时间:%d [%t] %n异常级别:%-5p &#xD;&#xA;异 常 类:%c [%x] %n%m %n" />
      </layout>
    </appender>

    <!-- Setup the root category, add the appenders and set the default level -->
    <root>
      <level value="ALL" />
      <appender-ref ref="ConsoleAppender" />
      <appender-ref ref="FileAppender" />
      <appender-ref ref="RollingLogFileAppender" />
    </root>

  </log4net>
</configuration>

3、在Startup.cs类中注册服务,如下:

        public static ILoggerRepository repository { get; set; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            // 指定配置文件
            repository = LogManager.CreateRepository("NETCoreRepository");
            XmlConfigurator.Configure(repository, new FileInfo("Log4Net.config"));
        }

需要引入命名空间:

using log4net;
using log4net.Config;
using log4net.Repository;

3、在控制器中依赖注入,并书写日志:

    public class MiddController : Controller
    {
        private ILog log;
        public MiddController()
        {
            this.log = LogManager.GetLogger(Startup.repository.Name, typeof(HomeController));
        }
        public IActionResult Index()
        {
            log.Error("测试日志");
            log.Info("测试日志");
            try
            {
                int a = 0, b = 5;
                var result = b / a;
            }
            catch (Exception)
            {
                throw new ArgumentException("被除数不能为0", "路径:Manger/index");
            }
            return View();
        }
    }

系统中会生成:

 

最后,我们将日志与异常处理过滤器和中间件相结合,这样,当项目发生异常的时候,我们可以将异常信息写入到日志中了。

 哈哈,今天就这么多了,一篇博客写了两天了,主要是在公司太忙了,够够了,打算跳槽,但今年经济又不好,妈的,打算换个轻松的,看领导的意思了。

最后,我发誓,今年一定要自学完NetCore。

@天才卧龙的博客

posted @ 2020-06-10 15:29  天才卧龙  阅读(1284)  评论(0编辑  收藏  举报