十年河东,十年河西,莫欺少年穷
学无止境,精益求精
背景
作为开发者,你兴高采烈地完成了新系统的功能开发。并且顺利经过验收,系统如期上线,皆大欢喜。
但是,有些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?}"); }); }