目錄
- 背景
- 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 選擇了要執行的操作之后運行。
過濾器類型
熟悉.NET MVC框架的同學應該知道,MVC也提供了5大過濾器供我們用來處理請求前后需要執行的代碼。分別是授權過濾器(AuthenticationFilter),資源過濾器(resource-filters),操作過濾器(ActionFilter),異常過濾器(ExceptionFilter),結果過濾器(ResultFilter)。
每種過濾選器類型都過濾器管道中的不同階段執行:
-
授權過濾器最先運行,用於確定是否已針對請求為用戶授權。 如果請求未獲授權,授權過濾器可以讓管道短路。
-
資源過濾器:
- 授權后運行。
- OnResourceExecuting 在過濾器管道的其余階段之前運行代碼。 例如,OnResourceExecuting 在模型綁定之前運行代碼。
- OnResourceExecuted 在管道的其余階段完成之后運行代碼。
-
操作過濾器:
- 在調用操作方法之前和之后立即運行代碼。
- 可以更改傳遞到操作中的參數。
- 可以更改從操作返回的結果。
- 不可在 Razor Pages 中使用。
-
異常過濾器在向響應正文寫入任何內容之前,對未經處理的異常應用全局策略。
結果過濾器在執行操作結果之前和之后立即運行代碼。 僅當操作方法成功執行時,它們才會運行。 對於必須圍繞視圖或格式化程序的執行的邏輯,它們很有用。
下圖展示過濾器類型在篩選器管道中的交互方式。
過濾器使用
在.net core 中,一般是在StartUp.cs的ConfigureServices方法中注冊
// 將異常過濾器注入到容器中
services.AddScoped<GlobalExceptionFilter>();
中間件
中間件(Middleware)的作用
ASP.NETCore應用基於一系列中間件構建。中間件是排列到管道中的處理程序,用於處理請求和響應。 在 Web 窗體應用程序中,HTTP 處理程序和模塊解決了類似的問題。 在 ASP.NET Core 中,模塊、處理程序、 Global.asax.cs和應用程序生命周期替換為中間件。
ASP.NET Core 請求管道包含一系列請求委托,依次調用。 下圖演示了這一概念。 沿黑色箭頭執行。
可以看到,每一個中間件都都可以在請求之前和之后進行操作。請求處理完成之后傳遞給下一個請求。
中間件的運行方式
默認情況下,中間件的執行順序根據Startup.cs中,Configure方法中注冊的先后順序執行。一般通過aApp.UseMiddleware<>()的方式注冊中間件。
// ExceptionMiddleware 加入管道
app.UseMiddleware<ExceptionMiddleware>();
使用ExceptionFilter
前面提到,過濾器可以處理錯誤異常。這里可以實踐一把。
新建一個.NET Core MVC控制器(.net WebAPI也類似)。
我在Test/Index Action方法中故意制造一個異常(我們知道在被除數不能為0).
public IActionResult Index()
{
int a = 0, b = 5;
var result = b/a;
}
在Visual Studio中調試報錯了
我們深知,異常這樣報錯很不友好,於是我們用了萬能的try-catch
public IActionResult Index()
{
try
{
int a = 0, b = 5;
var result = b / a;
} catch (Exception)
{
throw new ArgumentException("被除數不能為0", "a");
}
}
這樣異常提示確實友好了,並且我們攔截了異常,甚至可以將異常記錄到日志中。
但是每個方法都這樣加會不會覺得很煩?有沒有想過一勞永逸的辦法。從架構層面應該這樣思考。
在傳統的 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;
}
/// <summary>
/// 發生異常進入
/// </summary>
/// <param name="context"></param>
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<GlobalExceptionFilter>();
然后在需要的控制器上加上特性**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<ExceptionMiddleware>();
啟動調試,結果如下
{"message":"Attempted to divide by zero."}
統一封裝冷異常處理方式和消息格式,對前端也很友好。
總結
通過依賴注入和管道中間件兩種不同的全局捕獲異常處理。實際項目中,也是應當區分不同的業務場景,輸出不同的日志信息,不管是從安全或者是用戶體驗友好性上面來說,都是非常值得推薦的方式,全局異常捕獲處理,完全和業務剝離。
從運維的角度看,將異常處理的日志進行統一采集和分類,便於接入ELK,或者第三方日志系統。方便檢測日志,從而監測系統健康狀況。