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