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 選擇了要執行的操作之后運行。

使用場景

過濾器類型

  熟悉.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,或者第三方日志系統。方便檢測日志,從而監測系統健康狀況。

參考

使用場景


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM