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。

@天才卧龍的博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM