十年河東,十年河西,莫欺少年窮
學無止境,精益求精
背景
作為開發者,你興高采烈地完成了新系統的功能開發。並且順利經過驗收,系統如期上線,皆大歡喜。
但是,有些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?}"); }); }
啟動調試后,也會出現如下頁面:
日志如下:
控制器代碼如下:

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(); } }
總之
通過依賴注入和管道中間件兩種不同的全局捕獲異常處理。實際項目中,也是應當區分不同的業務場景,輸出不同的日志信息,不管是從安全或者是用戶體驗友好性上面來說,都是非常值得推薦的方式,全局異常捕獲處理,完全和業務剝離。
從運維的角度看,將異常處理的日志進行統一采集和分類,便於接入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 
異 常 類:%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。
@天才卧龍的博客