ASP.NET Core 中介軟體自定義全域性異常處理
阿新 • • 發佈:2020-03-08
## 目錄
- 背景
- 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