1. 程式人生 > >基於 abp vNext 和 .NET Core 開發部落格專案 - 異常處理和日誌記錄

基於 abp vNext 和 .NET Core 開發部落格專案 - 異常處理和日誌記錄

在開始之前,我們實現一個之前的遺留問題,這個問題是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出來的,就是當我們對Swagger進行分組,實現`IDocumentFilter`介面添加了文件描述資訊後,切換分組時會顯示不屬於當前分組的Tag。 經過研究和分析發現,是可以解決的,我不知道大家有沒有更好的辦法,我的實現方法請看: ![0](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523170024363-1101269881.png) ```CSharp //SwaggerDocumentFilter.cs ... public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { var tags = new List{...} #region 實現新增自定義描述時過濾不屬於同一個分組的API var groupName = context.ApiDescriptions.FirstOrDefault().GroupName; var apis = context.ApiDescriptions.GetType().GetField("_source", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.ApiDescriptions) as IEnumerable; 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。 然後使用`GetType().GetField(string name, BindingFlags bindingAttr)`獲取到`_source`,當前專案的所有API,裡面同時也包含了ABP預設生成的一些介面。 再將API中不屬於當前分組的API篩選掉,用Select查詢出所有的Controller名稱進行去重。 因為`OpenApiTag`中的Name名稱與Controller的Name是一致的,所以最後將包含`controllers`名稱的tag查詢出來取反,即可滿足需求。 --- 上一篇文章(https://www.cnblogs.com/meowv/p/12935693.html)集成了GitHub,使用JWT的方式完成了身份認證和授權,保護了我們寫的API介面。 本篇主要實現對專案中出現的異常僅需處理,當出現不可避免的錯誤時,或者未授權使用者呼叫介面時,可以進行有效的監控和日誌記錄。 目前呼叫未授權介面,會直接返回一個狀態碼為401的錯誤頁面,這樣顯得太不友好,我們還是用之前寫的統一返回模型來告訴呼叫者,你是未授權的,調不了我的介面,上篇也有提到過,我們將用兩種方式來解決。 **方式一** :使用`AddJwtBearer()`擴充套件方法下面的`options.Events`事件機制。 ```CSharp //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,並返回模型資料。 ![1](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523172916427-827941643.png) 如圖所示,可以看到已經成功返回了一段比較友好的JSON資料。 ```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 { /// /// 異常處理中介軟體 ///
public class ExceptionHandlerMiddleware { private readonly RequestDelegate next; public ExceptionHandlerMiddleware(RequestDelegate next) { this.next = next; } /// /// Invoke /// /// /// 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()); } } } /// /// 異常處理,返回JSON ///
/// /// /// 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(...)`中使用它。 ```CSharp public override void OnApplicationInitialization(ApplicationInitializationContext context) { ... // 異常處理中介軟體 app.UseMiddleware(); ... } ``` 同樣可以達到效果,相比之下他還支援狀態非401的錯誤返回,比如我們訪問一個不存在的頁面:https://localhost:44388/aaa ,也可以友好的進行處理。 ![2](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523175616269-2001433886.png) 當然這兩種方式可以共存,互不影響。 還有一種處理異常的方式,就是我們的過濾器Filter,abp已經預設為我們實現了全域性的異常模組,詳情可以看其文件:https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling ,在這裡,我準備移除abp提供的異常處理模組,自己實現一個。 先看一下目前的異常顯示情況,我們在`HelloWorldController`中寫一個異常介面。 ```CSharp //HelloWorldController.cs ... [HttpGet] [Route("Exception")] public string Exception() { throw new NotImplementedException("這是一個未實現的異常介面"); } ... ``` ![3](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523181756975-810381804.png) 按理說,他應該會執行到我們寫的`ExceptionHandlerMiddleware`中介軟體中去,但是被我們的Filter進行攔截了,現在我們移除預設的攔截器`AbpExceptionFilter` 還是在模組類`MeowvBlogHttpApiHostingModule`,`ConfigureServices()`方法中。 ```CSharp Configure(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掉,此時再看一下有異常的介面。 ![4](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523182414745-1132702809.png) 當我們註釋掉我們的中介軟體時,他就會顯示如下圖這樣。 ![5](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523182627715-1392661032.png) 這個頁面有沒有很熟悉的感覺?相信做過.net core開發的都遇到過吧。 ok,現在為止已經完美顯示了。但到這裡還遠遠不夠,說好的自己實現Filter呢?我們現在實現Filter又有什麼用呢?我們可以在Filter中可以做一些日誌記錄。 在`.HttpApi.Hosting`層新增資料夾Filters,新建一個`MeowvBlogExceptionFilter.cs`的Filter,他需要實現我們的`IExceptionFilter`介面的`OnExceptionAsync()`方法即可。 ```CSharp //MeowvBlogExceptionFilter.cs using Meowv.Blog.ToolKits.Helper; using Microsoft.AspNetCore.Mvc.Filters; namespace Meowv.Blog.HttpApi.Hosting.Filters { public class MeowvBlogExceptionFilter : IExceptionFilter { /// /// 異常處理 ///
/// /// public void OnException(ExceptionContext context) { // 日誌記錄 LoggerHelper.WriteToFile($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception); } } } ``` `OnException(...)`方法很簡單,這裡只做了記錄日誌的操作,剩下的交給我們中介軟體去處理吧。 注意,一定要在移除預設`AbpExceptionFilter`後,將我們自己實現的`MeowvBlogExceptionFilter`在模組類`ConfigureServices()`方法中注入到系統。 ```CSharp ... Configure(options => { ... // 新增自己實現的 MeowvBlogExceptionFilter options.Filters.Add(typeof(MeowvBlogExceptionFilter)); }); ... ``` 說到日誌,就有很多種處理方式,**請選擇你熟悉的方式**,我這裡將使用`log4net`進行處理,僅供參考。 在`.ToolKits`層新增`log4net`包,使用命令安裝:`Install-Package log4net`,然後新增資料夾Helper,新建一個`LoggerHelper.cs`。 ```CSharp //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")); } /// /// 寫日誌 /// /// /// public static void WriteToFile(string message) { Log.Info(message); } /// /// 寫日誌 /// /// /// public static void WriteToFile(string message, Exception ex) { if (string.IsNullOrEmpty(message)) message = ex.Message; Log.Error(message, ex); } } } ``` 在`.HttpApi.Hosting`中新增log4net配置檔案,`log4net.config`配置檔案如下: ```xml //log4net.config
``` 此時再去呼叫 .../HelloWorld/Exception,將會得到日誌檔案,內容是以JSON格式進行儲存的。 ![6](https://img2020.cnblogs.com/blog/891843/202005/891843-20200523203231072-1120908287.png) 關於Filter的更多用法可以參考微軟官方文件:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters 到這裡,系統的異常處理和日誌記錄便完成了,你學會了嗎?