1. 程式人生 > >ASP.NET Core 中介軟體自定義全域性異常處理

ASP.NET Core 中介軟體自定義全域性異常處理

## 目錄 - 背景 - ASP.NET Core過濾器(Filter) - ASP.NET Core 中介軟體(Middleware) - 自定義全域性異常處理 - .Net Core中使用ExceptionFilter - .Net Core中使用中介軟體 - 總結 - 參考 ## 背景   作為開發者,你興高采烈地完成了新系統的功能開發。並且順利經過驗收,系統如期上線,皆大歡喜。   但是,有些bug就是在生產環境如期而至了。半夜夢酣之時,你被運維童鞋的電話驚醒了,系統不能正常運行了。接下來,他打包了一堆日誌檔案給你...   筆者有幸做過幾年運維自動化系統,深知產品的每一次大跌代上線都是一場很多IT人的噩夢。更甚者,開發和運維人員有時候因為定位一個線上問題,花了一個通宵或者甚至版本回退。   **幹了多年開發越來越覺得,異常處理和定位的能力反映出開發者硬核能力**。如果開發人員能夠在對系統中異常進行捕獲,然後記錄日誌,並對日誌進行劃分等級,然後通過郵件或者簡訊等提醒,是不是能夠做到提前預判呢。    在 asp.net core中全域性異常處理,這裡介紹兩種不同的處理方式:過濾器捕獲和中介軟體過濾。 ## 過濾器   通過使用 ASP.NET Core 中的篩選器,可在請求處理管道中的特定階段之前或之後執行程式碼。   內建過濾器處理任務,例如: - 授權(防止使用者訪問未獲授權的資源)。 - 響應快取(對請求管道進行短路出路,以便返回快取的響應)。   可以建立自定義過濾器,用於處理橫切關注點。 橫切關注點的示例包括錯誤處理、快取、配置、授權和日誌記錄。 過濾器可以避免複製程式碼。 例如,錯誤處理異常過濾器可以合併錯誤處理。 #### 過濾器的工作原理   過濾器在 ASP.NET Core 操作呼叫管道(有時稱過濾器管道)內執行。 過濾器管道在 ASP.NET Core 選擇了要執行的操作之後執行。 ![使用場景](http://pepper.img.zhikestreet.com/filter-pipeline-1.png) #### 過濾器型別   熟悉.NET MVC框架的同學應該知道,MVC也提供了5大過濾器供我們用來處理請求前後需要執行的程式碼。分別是授權過濾器(AuthenticationFilter),資源過濾器(resource-filters),操作過濾器(ActionFilter),異常過濾器(ExceptionFilter),結果過濾器(ResultFilter)。   每種過濾選器型別都過濾器管道中的不同階段執行: - 授權過濾器最先執行,用於確定是否已針對請求為使用者授權。 如果請求未獲授權,授權過濾器可以讓管道短路。 - **資源過濾器**: - 授權後執行。 - OnResourceExecuting 在過濾器管道的其餘階段之前執行程式碼。 例如,OnResourceExecuting 在模型繫結之前執行程式碼。 - OnResourceExecuted 在管道的其餘階段完成之後執行程式碼。 - **操作過濾器**: - 在呼叫操作方法之前和之後立即執行程式碼。 - 可以更改傳遞到操作中的引數。 - 可以更改從操作返回的結果。 - 不可在 Razor Pages 中使用。 - **異常過濾器**在向響應正文寫入任何內容之前,對未經處理的異常應用全域性策略。 結果過濾器在執行操作結果之前和之後立即執行程式碼。 僅當操作方法成功執行時,它們才會執行。 對於必須圍繞檢視或格式化程式的執行的邏輯,它們很有用。   下圖展示過濾器型別在篩選器管道中的互動方式。 ![使用場景](http://pepper.img.zhikestreet.com/filter-pipeline-2.png) #### 過濾器使用   在.net core 中,一般是在StartUp.cs的ConfigureServices方法中註冊 ``` // 將異常過濾器注入到容器中 services.AddScoped(); ``` ## 中介軟體 #### 中介軟體(Middleware)的作用   ASP.NETCore應用基於一系列中介軟體構建。中介軟體是排列到管道中的處理程式,用於處理請求和響應。 在 Web 窗體應用程式中,HTTP 處理程式和模組解決了類似的問題。 在 ASP.NET Core 中,模組、處理程式、 Global.asax.cs和應用程式生命週期替換為中介軟體。   ASP.NET Core 請求管道包含一系列請求委託,依次呼叫。 下圖演示了這一概念。 沿黑色箭頭執行。 ![使用場景](http://pepper.img.zhikestreet.com/aspnet%20core%20%E7%AE%A1%E9%81%93.png)   可以看到,每一箇中間件都都可以在請求之前和之後進行操作。請求處理完成之後傳遞給下一個請求。 #### 中介軟體的執行方式   預設情況下,中介軟體的執行順序根據Startup.cs中,Configure方法中註冊的先後順序執行。一般通過aApp.UseMiddleware<>()的方式註冊中介軟體。 ``` // ExceptionMiddleware 加入管道 app.UseMiddleware(); ``` ## 使用ExceptionFilter   前面提到,過濾器可以處理錯誤異常。這裡可以實踐一把。   新建一個.NET Core MVC控制器(.net WebAPI也類似)。   我在Test/Index Action方法中故意製造一個異常(我們知道在被除數不能為0). ``` public IActionResult Index() { int a = 0, b = 5; var result = b/a; } ```   在Visual Studio中除錯報錯了 ![使用場景](http://pepper.img.zhikestreet.com/Attempted%20to%20divide%20by%20zero.png)   我們深知,異常這樣報錯很不友好,於是我們用了萬能的try-catch ``` public IActionResult Index() { try { int a = 0, b = 5; var result = b / a; } catch (Exception) { throw new ArgumentException("被除數不能為0", "a"); } } ```   這樣異常提示確實友好了,並且我們攔截了異常,甚至可以將異常記錄到日誌中。 ![使用場景](http://pepper.img.zhikestreet.com/%E8%A2%AB%E9%99%A4%E6%95%B0%E4%B8%8D%E8%83%BD%E4%B8%BA0.png)   但是每個方法都這樣加會不會覺得很煩?有沒有想過一勞永逸的辦法。從架構層面應該這樣思考。   在傳統的 Asp.Net MVC 應用程式中,我們一般都使用服務過濾的方式去捕獲和處理異常,這種方式 非常常見,而且可用性來說,體驗也不錯,幸運的是 Asp.Net Core也完整的支援該方式。 新建一個全域性異常過濾器GlobalExceptionFilter.cs,繼承自IExceptionFilter。 ``` public class GlobalExceptionFilter:Attribute, IExceptionFilter { private readonly IHostingEnvironment _hostingEnvironment; private readonly IModelMetadataProvider _modelMetadataProvider; public GlobalExceptionFilter( IHostingEnvironment hostingEnvironment, IModelMetadataProvider modelMetadataProvider) { _hostingEnvironment = hostingEnvironment; _modelMetadataProvider = modelMetadataProvider; } /// /// 發生異常進入 ///
/// public async 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 }; result.Content = JsonConvert.SerializeObject(json); } else { result.Content = "抱歉,出錯了"; } context.Result = result; context.ExceptionHandled = true; } } ```   我們在startup.cs中進行中注入 ``` // 將異常過濾器注入到容器中 services.AddScoped(); ```   然後在需要的控制器上加上特性**ServiceFilter(typeof(GlobalExceptionFilter))] ``` [ServiceFilter(typeof(GlobalExceptionFilter))] public class TestController : Controller ```   啟動程式,錯誤提示,頁面只會顯示單純錯誤資訊。 > {"message":"Attempted to divide by zero."} ## Net Core中使用中介軟體方式   首先,建立一箇中間件ExceptionMiddleware ``` public class ExceptionMiddleware { private readonly RequestDelegate next; private IHostingEnvironment environment; public ExceptionMiddleware(RequestDelegate next,IHostingEnvironment environment) { 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}; error = JsonConvert.SerializeObject(json); } else error = "抱歉,出錯了"; await context.Response.WriteAsync(error); } } ```   建立 HandleException(HttpContext context, Exception e) 處理異常,判斷是 Development 環境下,輸出詳細的錯誤資訊,非 Development 環境僅提示呼叫者“抱歉,出錯了”,同時使用 NLog 元件將日誌寫入硬碟;同樣,在 Startup.cs 中將 ExceptionMiddleware 加入管道中 ``` //ExceptionMiddleware 加入管道 app.UseMiddleware(); ``` 啟動除錯,結果如下 ``` {"message":"Attempted to divide by zero."} ```   統一封裝冷異常處理方式和訊息格式,對前端也很友好。 ## 總結   通過依賴注入和管道中介軟體兩種不同的全域性捕獲異常處理。實際專案中,也是應當區分不同的業務場景,輸出不同的日誌資訊,不管是從安全或者是使用者體驗友好性上面來說,都是非常值得推薦的方式,全域性異常捕獲處理,完全和業務剝離。   從運維的角度看,將異常處理的日誌進行統一採集和分類,便於接入ELK,或者第三方日誌系統。方便檢測日誌,從而監測系統健康狀況。 ## 參考 -   [ASP.NET Core中的篩選器](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1#resource-filters) -   [ASP.NET Core中介軟體](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) -   [Asp.net Core全域性異常監控和記錄日誌](https://www.cnblogs.com/sword-successful/p/11771858.html) -   [智客工坊](http://www.52interview.com/) ![使用場景](http://pepper.img.zhikestreet.com/ZhiKeCo