在開始以前,咱們實現一個以前的遺留問題,這個問題是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出來的,就是當咱們對Swagger進行分組,實現IDocumentFilter
接口添加了文檔描述信息後,切換分組時會顯示不屬於當前分組的Tag。html
通過研究和分析發現,是能夠解決的,我不知道你們有沒有更好的辦法,個人實現方法請看:git
//SwaggerDocumentFilter.cs ... public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { var tags = new List<OpenApiTag>{...} #region 實現添加自定義描述時過濾不屬於同一個分組的API var groupName = context.ApiDescriptions.FirstOrDefault().GroupName; var apis = context.ApiDescriptions.GetType().GetField("_source", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.ApiDescriptions) as IEnumerable<ApiDescription>; var controllers = apis.Where(x => x.GroupName != groupName).Select(x => ((ControllerActionDescriptor)x.ActionDescriptor).ControllerName).Distinct(); swaggerDoc.Tags = tags.Where(x => !controllers.Contains(x.Name)).OrderBy(x => x.Name).ToList(); #endregion } ...
根據調試代碼發現,咱們能夠從context.ApiDescriptions
獲取到當前顯示的是哪個分組下的API。github
而後使用GetType().GetField(string name, BindingFlags bindingAttr)
獲取到_source
,當前項目的全部API,裏面同時也包含了ABP默認生成的一些接口。json
再將API中不屬於當前分組的API篩選掉,用Select查詢出全部的Controller名稱進行去重。api
由於OpenApiTag
中的Name名稱與Controller的Name是一致的,因此最後將包含controllers
名稱的tag查詢出來取反,便可知足需求。mvc
上一篇文章(http://www.javashuo.com/article/p-ecngxpov-ev.html)集成了GitHub,使用JWT的方式完成了身份認證和受權,保護了咱們寫的API接口。app
本篇主要實現對項目中出現的異常僅需處理,當出現不可避免的錯誤時,或者未受權用戶調用接口時,能夠進行有效的監控和日誌記錄。異步
目前調用未受權接口,會直接返回一個狀態碼爲401的錯誤頁面,這樣顯得太不友好,咱們仍是用以前寫的統一返回模型來告訴調用者,你是未受權的,調不了個人接口,上篇也有提到過,咱們將用兩種方式來解決。async
方式一 :使用AddJwtBearer()
擴展方法下面的options.Events
事件機制。ide
//MeowvBlogHttpApiHostingModule.cs ... //應用程序提供的對象,用於處理承載引起的事件,身份驗證處理程序 options.Events = new JwtBearerEvents { OnChallenge = async context => { // 跳過默認的處理邏輯,返回下面的模型數據 context.HandleResponse(); context.Response.ContentType = "application/json;charset=utf-8"; context.Response.StatusCode = StatusCodes.Status200OK; var result = new ServiceResult(); result.IsFailed("UnAuthorized"); await context.Response.WriteAsync(result.ToJson()); } }; ...
在項目啓動時,實例化了OnChallenge
,若是用戶調用未受權,將請求的狀態碼賦值爲200,並返回模型數據。
如圖所示,能夠看到已經成功返回了一段比較友好的JSON數據。
{ "Code": 1, "Message": "UnAuthorized", "Success": false, "Timestamp": 1590226085318 }
方式二 :使用中間件的方式。
咱們註釋掉上面的代碼,在.HttpApi.Hosting
添加文件夾Middleware,新建一箇中間件ExceptionHandlerMiddleware.cs
using Meowv.Blog.ToolKits.Base; using Meowv.Blog.ToolKits.Extensions; using Microsoft.AspNetCore.Http; using System; using System.Net; using System.Threading.Tasks; namespace Meowv.Blog.HttpApi.Hosting.Middleware { /// <summary> /// 異常處理中間件 /// </summary> public class ExceptionHandlerMiddleware { private readonly RequestDelegate next; public ExceptionHandlerMiddleware(RequestDelegate next) { this.next = next; } /// <summary> /// Invoke /// </summary> /// <param name="context"></param> /// <returns></returns> public async Task Invoke(HttpContext context) { try { await next(context); } catch (Exception ex) { await ExceptionHandlerAsync(context, ex.Message); } finally { var statusCode = context.Response.StatusCode; if (statusCode != StatusCodes.Status200OK) { Enum.TryParse(typeof(HttpStatusCode), statusCode.ToString(), out object message); await ExceptionHandlerAsync(context, message.ToString()); } } } /// <summary> /// 異常處理,返回JSON /// </summary> /// <param name="context"></param> /// <param name="message"></param> /// <returns></returns> private async Task ExceptionHandlerAsync(HttpContext context, string message) { context.Response.ContentType = "application/json;charset=utf-8"; var result = new ServiceResult(); result.IsFailed(message); await context.Response.WriteAsync(result.ToJson()); } } }
RequestDelegate
是一種請求委託類型,用來處理HTTP請求的函數,返回的是delegate
,實現異步的Invoke
方法。
這裏我寫了一個比較通用的方法,當出現異常時直接執行ExceptionHandlerAsync()
方法,當沒有異常發生時,在finally
中判斷當前請求狀態,多是200?404?401?等等,無論它是什麼,反正不是200,獲取到狀態碼枚舉的Key值用來看成錯誤信息返回,最後也執行ExceptionHandlerAsync()
方法,返回咱們自定義的模型。
寫好了中間件,而後在OnApplicationInitialization(...)
中使用它。
public override void OnApplicationInitialization(ApplicationInitializationContext context) { ... // 異常處理中間件 app.UseMiddleware<ExceptionHandlerMiddleware>(); ... }
一樣能夠達到效果,相比之下他還支持狀態非401的錯誤返回,好比咱們訪問一個不存在的頁面:https://localhost:44388/aaa ,也能夠友好的進行處理。
固然這兩種方式能夠共存,互不影響。
還有一種處理異常的方式,就是咱們的過濾器Filter,abp已經默認爲咱們實現了全局的異常模塊,詳情能夠看其文檔:https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling ,在這裏,我準備移除abp提供的異常處理模塊,本身實現一個。
先看一下目前的異常顯示狀況,咱們在HelloWorldController
中寫一個異常接口。
//HelloWorldController.cs ... [HttpGet] [Route("Exception")] public string Exception() { throw new NotImplementedException("這是一個未實現的異常接口"); } ...
按理說,他應該會執行到咱們寫的ExceptionHandlerMiddleware
中間件中去,可是被咱們的Filter進行攔截了,如今咱們移除默認的攔截器AbpExceptionFilter
仍是在模塊類MeowvBlogHttpApiHostingModule
,ConfigureServices()
方法中。
Configure<MvcOptions>(options => { var filterMetadata = options.Filters.FirstOrDefault(x => x is ServiceFilterAttribute attribute && attribute.ServiceType.Equals(typeof(AbpExceptionFilter))); // 移除 AbpExceptionFilter options.Filters.Remove(filterMetadata); });
從options.Filters
中找到AbpExceptionFilter
,而後Remove掉,此時再看一下有異常的接口。
當咱們註釋掉咱們的中間件時,他就會顯示以下圖這樣。
這個頁面有沒有很熟悉的感受?相信作過.net core開發的都遇到過吧。
ok,如今爲止已經完美顯示了。但到這裏還遠遠不夠,說好的本身實現Filter呢?咱們如今實現Filter又有什麼用呢?咱們能夠在Filter中能夠作一些日誌記錄。
在.HttpApi.Hosting
層添加文件夾Filters,新建一個MeowvBlogExceptionFilter.cs
的Filter,他須要實現咱們的IExceptionFilter
接口的OnExceptionAsync()
方法便可。
//MeowvBlogExceptionFilter.cs using Meowv.Blog.ToolKits.Helper; using Microsoft.AspNetCore.Mvc.Filters; namespace Meowv.Blog.HttpApi.Hosting.Filters { public class MeowvBlogExceptionFilter : IExceptionFilter { /// <summary> /// 異常處理 /// </summary> /// <param name="context"></param> /// <returns></returns> public void OnException(ExceptionContext context) { // 日誌記錄 LoggerHelper.WriteToFile($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception); } } }
OnException(...)
方法很簡單,這裏只作了記錄日誌的操做,剩下的交給咱們中間件去處理吧。
注意,必定要在移除默認AbpExceptionFilter
後,將咱們本身實現的MeowvBlogExceptionFilter
在模塊類ConfigureServices()
方法中注入到系統。
... Configure<MvcOptions>(options => { ... // 添加本身實現的 MeowvBlogExceptionFilter options.Filters.Add(typeof(MeowvBlogExceptionFilter)); }); ...
說到日誌,就有不少種處理方式,請選擇你熟悉的方式,我這裏將使用log4net
進行處理,僅供參考。
在.ToolKits
層添加log4net
包,使用命令安裝:Install-Package log4net
,而後添加文件夾Helper,新建一個LoggerHelper.cs
。
//LoggerHelper.cs using log4net; using log4net.Config; using log4net.Repository; using System; using System.IO; namespace Meowv.Blog.ToolKits.Helper { public static class LoggerHelper { private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository"); private static readonly ILog Log = LogManager.GetLogger(Repository.Name, "NETCorelog4net"); static LoggerHelper() { XmlConfigurator.Configure(Repository, new FileInfo("log4net.config")); } /// <summary> /// 寫日誌 /// </summary> /// <param name="message"></param> /// <param name="ex"></param> public static void WriteToFile(string message) { Log.Info(message); } /// <summary> /// 寫日誌 /// </summary> /// <param name="message"></param> /// <param name="ex"></param> public static void WriteToFile(string message, Exception ex) { if (string.IsNullOrEmpty(message)) message = ex.Message; Log.Error(message, ex); } } }
在.HttpApi.Hosting
中添加log4net配置文件,log4net.config
配置文件以下:
//log4net.config <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/> </configSections> <log4net debug="false"> <appender name="info" type="log4net.Appender.RollingFileAppender,log4net"> <param name="File" value="log4net/info/" /> <param name="AppendToFile" value="true" /> <param name="MaxSizeRollBackups" value="-1"/> <param name="MaximumFileSize" value="5MB"/> <param name="RollingStyle" value="Composite" /> <param name="DatePattern" value="yyyyMMdd\\HH".log"" /> <param name="StaticLogFileName" value="false" /> <layout type="log4net.Layout.PatternLayout,log4net"> <param name="ConversionPattern" value="%n { "system": "Meowv.Blog", "datetime": "%d", "description": "%m", "level": "%p", "info": "%exception" }" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="INFO" /> <levelMax value="INFO" /> </filter> </appender> <appender name="error" type="log4net.Appender.RollingFileAppender,log4net"> <param name="File" value="log4net/error/" /> <param name="AppendToFile" value="true" /> <param name="MaxSizeRollBackups" value="-1"/> <param name="MaximumFileSize" value="5MB"/> <param name="RollingStyle" value="Composite" /> <param name="DatePattern" value="yyyyMMdd\\HH".log"" /> <param name="StaticLogFileName" value="false" /> <layout type="log4net.Layout.PatternLayout,log4net"> <param name="ConversionPattern" value="%n { "system": "Meowv.Blog", "datetime": "%d", "description": "%m", "level": "%p", "info": "%exception" }" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="ERROR" /> <levelMax value="ERROR" /> </filter> </appender> <root> <level value="ALL"></level> <appender-ref ref="info"/> <appender-ref ref="error"/> </root> </log4net> </configuration>
此時再去調用 .../HelloWorld/Exception,將會獲得日誌文件,內容是以JSON格式進行存儲的。
關於Filter的更多用法能夠參考微軟官方文檔:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters
到這裏,系統的異常處理和日誌記錄便完成了,你學會了嗎?😁😁😁