前言
全局異常處理是我們程式設計過程中不可或缺的重要環節。有了全局異常處理機制給我們帶來了很多便捷,首先我們不用滿螢幕處理程式可能出現的異常,其次我們可以對異常進行統一的處理,比如收集異常資訊或者傳回統一的格式等等。ASP.NET Core為我們提供了兩種機制去處理全局異常,一是基于中間件的方式,二是基于Filter過濾器的方式。Filter過濾器的方式相對來說比較簡單,就是捕獲Action執行過程中出現的異常,然後調用注冊的Filter去執行處理異常資訊,在這裡就不過多介紹這種方式了,接下來我們主要介紹中間件的方式。
異常進行中間件
ASP.NET Core為我們提供了幾種不同處理異常方式的中間件分别是UseDeveloperExceptionPage、UseExceptionHandler、UseStatusCodePages、UseStatusCodePagesWithRedirects、UseStatusCodePagesWithReExecute。這幾種方式處理的思路是一緻的都是通過捕獲該管道後續的管道執行過程中出現的異常,隻是處理的方式不一樣。一般推薦全局異常處理相關中間件寫到所有管道的最開始,這樣可以捕獲到整個執行管道過程中的異常資訊。接下來我們介紹一下最常用的三個異常進行中間件UseDeveloperExceptionPage、UseExceptionHandler、UseStatusCodePage。
UseDeveloperExceptionPage
UseDeveloperExceptionPage的使用場景大部分是開發階段,通過名稱我們就可以看出,通過它捕獲的異常會傳回一個異常界面,它的使用方式很簡單
//這個判斷不是必須的,但是在正式環境中給使用者展示代碼錯誤資訊,終究不是合理的 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
如果程式出現異常,出現的效果是這個樣子的

這裡包含了非常詳細的異常堆棧資訊、請求參數、Cookie資訊、Header資訊、和路由終結點相關的資訊。接下來我們找到UseDeveloperExceptionPage所在的擴充類
public static class DeveloperExceptionPageExtensions { public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app) { return app.UseMiddleware<DeveloperExceptionPageMiddleware>(); } public static IApplicationBuilder UseDeveloperExceptionPage( this IApplicationBuilder app, DeveloperExceptionPageOptions options) { return app.UseMiddleware<DeveloperExceptionPageMiddleware>(Options.Create(options)); } }
我們看到有兩個擴充方法一個是無參的,另一個是可以傳遞DeveloperExceptionPageOptions的擴充方法,因為平時使用無參的方法是以我們看下DeveloperExceptionPageOptions包含了哪些資訊,找到DeveloperExceptionPageOptions源碼
public class DeveloperExceptionPageOptions { public DeveloperExceptionPageOptions() { SourceCodeLineCount = 6; } /// <summary> /// 展示出現異常代碼的地方上下展示多少行的代碼資訊,預設是6行 /// </summary> public int SourceCodeLineCount { get; set; } /// <summary> /// 通過這個檔案提供程式我們可以猜測到,我們可以自定義異常錯誤界面 /// </summary> public IFileProvider FileProvider { get; set; } }
接下來我們就看核心的DeveloperExceptionPageMiddleware中間件大緻是如何工作的
public class DeveloperExceptionPageMiddleware { private readonly RequestDelegate _next; private readonly DeveloperExceptionPageOptions _options; private readonly ILogger _logger; private readonly IFileProvider _fileProvider; private readonly DiagnosticSource _diagnosticSource; private readonly ExceptionDetailsProvider _exceptionDetailsProvider; private readonly Func<ErrorContext, Task> _exceptionHandler; private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions<DeveloperExceptionPageOptions> options, ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable<IDeveloperPageExceptionFilter> filters) { _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger<DeveloperExceptionPageMiddleware>(); //預設使用ContentRootFileProvider _fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider; //可以發送診斷日志 _diagnosticSource = diagnosticSource; _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _options.SourceCodeLineCount); _exceptionHandler = DisplayException; //建構IDeveloperPageExceptionFilter執行管道,說明我們同時還可以通過程式的方式捕獲異常資訊 foreach (var filter in filters.Reverse()) { var nextFilter = _exceptionHandler; _exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter); } } public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { _logger.UnhandledException(ex); if (context.Response.HasStarted) { _logger.ResponseStartedErrorPageMiddleware(); throw; } try { //清除輸出相關資訊,将狀态碼設為500 context.Response.Clear(); context.Response.StatusCode = 500; //核心處理 await _exceptionHandler(new ErrorContext(context, ex)); //發送名稱為Microsoft.AspNetCore.Diagnostics.UnhandledException診斷日志,我們可以自定義訂閱者處理異常 if (_diagnosticSource.IsEnabled("Microsoft.AspNetCore.Diagnostics.UnhandledException")) { _diagnosticSource.Write("Microsoft.AspNetCore.Diagnostics.UnhandledException", new { httpContext = context, exception = ex }); } return; } catch (Exception ex2) { _logger.DisplayErrorPageException(ex2); } throw; } } }
通過上面代碼我們可以了解到我們可以通過自定義IDeveloperPageExceptionFilter的方式攔截到異常資訊做處理
public class MyDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter { private readonly ILogger<MyDeveloperPageExceptionFilter> _logger; public MyDeveloperPageExceptionFilter(ILogger<MyDeveloperPageExceptionFilter> logger) { _logger = logger; } public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next) { _logger.LogInformation($"狀态碼:{errorContext.HttpContext.Response.StatusCode},異常資訊:{errorContext.Exception.Message}"); await next(errorContext); } }
自定義的MyDeveloperPageExceptionFilter是需要注入的
services.AddSingleton<IDeveloperPageExceptionFilter,MyDeveloperPageExceptionFilter>();
同時還可以通過診斷日志的方式處理異常資訊,我使用了Microsoft.Extensions.DiagnosticAdapter擴充包,是以可以定義強類型類
public class DiagnosticCollector { private readonly ILogger<DiagnosticCollector> _logger; public DiagnosticCollector(ILogger<DiagnosticCollector> logger) { _logger = logger; } [DiagnosticName("Microsoft.AspNetCore.Diagnostics.UnhandledException")] public void OnRequestStart(HttpContext httpContext, Exception exception) { _logger.LogInformation($"診斷日志收集到異常,狀态碼:{httpContext.Response.StatusCode},異常資訊:{exception.Message}"); } }
通過這裡可以看出,異常處理擴充性還是非常強的,這僅僅是.Net Core設計方式的冰山一角。剛才我們提到_exceptionHandler才是處理的核心,通過構造函數可知這個委托是通過DisplayException方法初始化的,接下來我們看這裡的相關實作
private Task DisplayException(ErrorContext errorContext) { var httpContext = errorContext.HttpContext; var headers = httpContext.Request.GetTypedHeaders(); var acceptHeader = headers.Accept; //如果acceptHeader不存在或者類型不是text/plain,将以字元串的形式輸出異常,比如通過代碼或者Postman的方式調用并未設定頭資訊 if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType))) { httpContext.Response.ContentType = "text/plain"; var sb = new StringBuilder(); sb.AppendLine(errorContext.Exception.ToString()); sb.AppendLine(); sb.AppendLine("HEADERS"); sb.AppendLine("======="); foreach (var pair in httpContext.Request.Headers) { sb.AppendLine($"{pair.Key}: {pair.Value}"); } return httpContext.Response.WriteAsync(sb.ToString()); } //判斷是否為編譯時異常,比如視圖檔案可以動态編譯 if (errorContext.Exception is ICompilationException compilationException) { return DisplayCompilationException(httpContext, compilationException); } //處理運作時異常 return DisplayRuntimeException(httpContext, errorContext.Exception); }
關于DisplayCompilationException我們這裡就不做過多解釋了,在Asp.Net Core中cshtml檔案可以動态編譯,有興趣的同學可以自行了解。我們重點看下DisplayRuntimeException處理
private Task DisplayRuntimeException(HttpContext context, Exception ex) { //擷取終結點資訊 var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint; EndpointModel endpointModel = null; if (endpoint != null) { endpointModel = new EndpointModel(); endpointModel.DisplayName = endpoint.DisplayName; if (endpoint is RouteEndpoint routeEndpoint) { endpointModel.RoutePattern = routeEndpoint.RoutePattern.RawText; endpointModel.Order = routeEndpoint.Order; var httpMethods = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods; if (httpMethods != null) { endpointModel.HttpMethods = string.Join(", ", httpMethods); } } } var request = context.Request; //往視圖還是個輸出的模型,對于我們上面截圖展示的資訊對應的資料 var model = new ErrorPageModel { Options = _options, //異常詳情 ErrorDetails = _exceptionDetailsProvider.GetDetails(ex), //查詢參數相關 Query = request.Query, //Cookie資訊 Cookies = request.Cookies, //頭資訊 Headers = request.Headers, //路由資訊 RouteValues = request.RouteValues, //終結點資訊 Endpoint = endpointModel }; var errorPage = new ErrorPage(model); //執行輸出視圖頁面,也就是我們看到的開發者頁面 return errorPage.ExecuteAsync(context); }
其整體實作思路就是捕獲後續執行過程中出現的異常,如果出現異常則包裝異常資訊以及Http上下文和路由相關資訊到ErrorPageModel模型中,然後這個模型作為異常展示界面的資料模型進行展示。
UseExceptionHandler
UseExceptionHandler可能是我們在實際開發過程中使用最多的方式。UseDeveloperExceptionPage固然強大,但是傳回的終究還是供開發者使用的界面,通過UseExceptionHandler我們可以通過自己的方式處理異常資訊,這裡就需要我自己編碼
app.UseExceptionHandler(configure => { configure.Run(async context => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var ex = exceptionHandlerPathFeature?.Error; if (ex != null) { context.Response.ContentType = "text/plain;charset=utf-8"; await context.Response.WriteAsync(ex.ToString()); } }); }); //或 app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = async context => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var ex = exceptionHandlerPathFeature?.Error; if (ex != null) { context.Response.ContentType = "text/plain;charset=utf-8"; await context.Response.WriteAsync(ex.ToString()); } } }); //或 app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandlingPath = new PathString("/exception") });
通過上面的實作方式我們大概可以猜出擴充方法的幾種類型找到源碼位置ExceptionHandlerExtensions擴充類
public static class ExceptionHandlerExtensions { public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app) { return app.UseMiddleware<ExceptionHandlerMiddleware>(); } public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath) { return app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandlingPath = new PathString(errorHandlingPath) }); } public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, Action<IApplicationBuilder> configure) { //建立新的執行管道 var subAppBuilder = app.New(); configure(subAppBuilder); var exceptionHandlerPipeline = subAppBuilder.Build(); return app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = exceptionHandlerPipeline }); } public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, ExceptionHandlerOptions options) { return app.UseMiddleware<ExceptionHandlerMiddleware>(Options.Create(options)); } }
通過UseExceptionHandler擴充方法我們可以知道這麼多擴充方法其實本質都是在建構ExceptionHandlerOptions我們來看一下大緻實作
public class ExceptionHandlerOptions { /// <summary> /// 指定處理異常的終結點路徑 /// </summary> public PathString ExceptionHandlingPath { get; set; } /// <summary> /// 指定處理異常的終結點委托 /// </summary> public RequestDelegate ExceptionHandler { get; set; } }
這個類非常簡單,要麼指定處理異常資訊的具體終結點路徑,要麼自定義終結點委托處理異常資訊。通過上面的使用示例可以很清楚的看到這一點,接下來我們檢視一下ExceptionHandlerMiddleware中間件的大緻實作
public class ExceptionHandlerMiddleware { private readonly RequestDelegate _next; private readonly ExceptionHandlerOptions _options; private readonly ILogger _logger; private readonly Func<object, Task> _clearCacheHeadersDelegate; private readonly DiagnosticListener _diagnosticListener; public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticListener diagnosticListener) { _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>(); _clearCacheHeadersDelegate = ClearCacheHeaders; _diagnosticListener = diagnosticListener; //ExceptionHandler和ExceptionHandlingPath不同同時不存在 if (_options.ExceptionHandler == null) { if (_options.ExceptionHandlingPath == null) { throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly); } else { _options.ExceptionHandler = _next; } } } public Task Invoke(HttpContext context) { ExceptionDispatchInfo edi; try { var task = _next(context); //task未完成情況 if (!task.IsCompletedSuccessfully) { return Awaited(this, context, task); } return Task.CompletedTask; } catch (Exception exception) { edi = ExceptionDispatchInfo.Capture(exception); } return HandleException(context, edi); //處理未完成task static async Task Awaited(ExceptionHandlerMiddleware middleware, HttpContext context, Task task) { ExceptionDispatchInfo edi = null; try { //等待完成 await task; } catch (Exception exception) { //收集異常資訊 edi = ExceptionDispatchInfo.Capture(exception); } if (edi != null) { await middleware.HandleException(context, edi); } } } }
通過這段處理我們可以看出所有的異常處理都指向目前類的HandleException方法
private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi) { _logger.UnhandledException(edi.SourceException); // 如果輸出已經開始執行了,後續的代碼就沒必要執行了,直接重新抛出 if (context.Response.HasStarted) { _logger.ResponseStartedErrorHandler(); edi.Throw(); } PathString originalPath = context.Request.Path; //如果指定處理異常的終結點,将異常處理交給指定的終結點去處理 if (_options.ExceptionHandlingPath.HasValue) { //将處理路徑指向,異常處理終結點路徑 context.Request.Path = _options.ExceptionHandlingPath; } try { //清除原有HTTP上下文資訊,為了明确指定程式出現異常,防止異常未被處理而後續當做正常操作執行 ClearHttpContext(context); //将異常資訊包裝成ExceptionHandlerFeature,後續處理程式擷取異常資訊都是通過ExceptionHandlerFeature var exceptionHandlerFeature = new ExceptionHandlerFeature() { //異常資訊 Error = edi.SourceException, //原始路徑 Path = originalPath.Value, }; //将包裝的ExceptionHandlerFeature放入到上下文中,後續處理程式可通過HttpContext擷取異常資訊 context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature); context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature); //設定狀态碼 context.Response.StatusCode = 500; context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); //調用給定的異常處理終結點處理異常資訊 await _options.ExceptionHandler(context); //同樣也可以發送診斷日志,可以利用處理程式傳回輸出,診斷日志記錄異常将職責分離 if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled("Microsoft.AspNetCore.Diagnostics.HandledException")) { _diagnosticListener.Write("Microsoft.AspNetCore.Diagnostics.HandledException", new { httpContext = context, exception = edi.SourceException }); } return; } catch (Exception ex2) { _logger.ErrorHandlerException(ex2); } finally { //異常處理結束後,恢複原始的請求路徑,供後續執行程式能拿到原始的請求資訊 context.Request.Path = originalPath; } //如果異常沒被處理則重新抛出 edi.Throw(); }
最後還有一段清除上下文和清除輸出緩存的方法,因為程式一旦發生了異常,可能建立了新的終結點,是以執行管道會有所調整,是以需要重新計算。而且異常資訊保留輸出緩存是沒有意義的。
private static void ClearHttpContext(HttpContext context) { context.Response.Clear(); //因為可能建立了新的終結點,是以執行管道會有所調整,是以需要重新計算 context.SetEndpoint(endpoint: null); var routeValuesFeature = context.Features.Get<IRouteValuesFeature>(); routeValuesFeature?.RouteValues?.Clear(); } private static Task ClearCacheHeaders(object state) { //清除輸出緩存相關 var headers = ((HttpResponse)state).Headers; headers[HeaderNames.CacheControl] = "no-cache"; headers[HeaderNames.Pragma] = "no-cache"; headers[HeaderNames.Expires] = "-1"; headers.Remove(HeaderNames.ETag); return Task.CompletedTask; }
從上面的代碼我們可以看出UseExceptionHandler要比UseDeveloperExceptionPage實作方式簡單很多。其大緻思路就是捕獲後續管道執行異常,如果存在異常則将異常包裝成ExceptionHandlerFeature,放入到Http上下文中。之是以相對簡單主要原因還是UseExceptionHandler最終處理異常由我們自定義的終結點去處理,是以它隻是負責包裝異常相關資訊,并将它交于我們定義的異常處理終結點。
UseStatusCodePages
無論是UseDeveloperExceptionPage還是UseExceptionHandler都是通過捕獲異常的方式去處理異常資訊,UseStatusCodePages則是通過Http狀态碼去判斷是否為成功的傳回并進行處理,使用方式如下
app.UseStatusCodePages(); //或 app.UseStatusCodePages("text/plain;charset=utf-8", "狀态碼:{0}"); //或 app.UseStatusCodePages(async context => { context.HttpContext.Response.ContentType = "text/plain;charset=utf-8"; await context.HttpContext.Response.WriteAsync($"狀态碼:{context.HttpContext.Response.StatusCode}"); }); //或 app.UseStatusCodePages(new StatusCodePagesOptions { HandleAsync = async context=> { context.HttpContext.Response.ContentType = "text/plain;charset=utf-8"; await context.HttpContext.Response.WriteAsync($"狀态碼:{context.HttpContext.Response.StatusCode}"); }}); //或 app.UseStatusCodePages(configure => { configure.Run(async context => { await context.Response.WriteAsync($"狀态碼:{context.Response.StatusCode}"); }); });
接下來我們檢視一下UseStatusCodePages擴充方法相關實作
public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options) { return app.UseMiddleware<StatusCodePagesMiddleware>(Options.Create(options)); } public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app) { return app.UseMiddleware<StatusCodePagesMiddleware>(); } public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler) { return app.UseStatusCodePages(new StatusCodePagesOptions { HandleAsync = handler }); } public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat) { return app.UseStatusCodePages(context => { var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode); context.HttpContext.Response.ContentType = contentType; return context.HttpContext.Response.WriteAsync(body); }); }
雖然擴充方法比較多,但是本質都是組裝StatusCodePagesOptions,是以我們直接檢視源碼
public class StatusCodePagesOptions { public StatusCodePagesOptions() { //初始化 HandleAsync = context => { var statusCode = context.HttpContext.Response.StatusCode; var body = BuildResponseBody(statusCode); context.HttpContext.Response.ContentType = "text/plain"; return context.HttpContext.Response.WriteAsync(body); }; } private string BuildResponseBody(int httpStatusCode) { //組裝預設消息模闆 var internetExplorerWorkaround = new string(' ', 500); var reasonPhrase = ReasonPhrases.GetReasonPhrase(httpStatusCode); return string.Format(CultureInfo.InvariantCulture, "Status Code: {0}{1}{2}{3}", httpStatusCode, string.IsNullOrWhiteSpace(reasonPhrase) ? "" : "; ", reasonPhrase, internetExplorerWorkaround); } public Func<StatusCodeContext, Task> HandleAsync { get; set; } }
看着代碼不少,其實都是吓唬人的,就是給HandleAsync一個預設值,這個預設值裡有預設的輸出模闆。接下來我們檢視一下StatusCodePagesMiddleware中間件源碼
public class StatusCodePagesMiddleware { private readonly RequestDelegate _next; private readonly StatusCodePagesOptions _options; public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options) { _next = next; _options = options.Value; } public async Task Invoke(HttpContext context) { //初始化StatusCodePagesFeature var statusCodeFeature = new StatusCodePagesFeature(); context.Features.Set<IStatusCodePagesFeature>(statusCodeFeature); await _next(context); if (!statusCodeFeature.Enabled) { return; } //這個範圍外的Http狀态碼直接忽略,不受程式處理隻處理值為400-600之間的狀态碼 if (context.Response.HasStarted || context.Response.StatusCode < 400 || context.Response.StatusCode >= 600 || context.Response.ContentLength.HasValue || !string.IsNullOrEmpty(context.Response.ContentType)) { return; } //将狀态資訊包裝到StatusCodeContext,傳遞給自定義處理終結點 var statusCodeContext = new StatusCodeContext(context, _options, _next); await _options.HandleAsync(statusCodeContext); } }
這個中間件的實作思路更為簡單,主要就是攔截請求判斷Http狀态碼,判斷是否是400-600,也就是4xx 5xx相關的狀态碼,如果符合則包裝成StatusCodeContext,交由自定義的終結點去處理。
總結
關于常用異常進行中間件我們介紹到這裡就差不多了,接下來我們總結對比一下三種中間件的異同和大緻實作的方式
- UseDeveloperExceptionPage中間件主要工作方式就是捕獲後續中間件執行異常,如果存在異常則将異常資訊包裝成ErrorPageModel視圖模型,然後通過這個模型去渲染開發者異常界面。
- UseExceptionHandler中間件核心思路和UseDeveloperExceptionPage類似都是通過捕獲後續中間件執行異常,不同之處在于UseExceptionHandler将捕獲的異常資訊包裝到ExceptionHandlerFeature然後将其放入Http上下文中,後續的異常處理終結點通過Http上下文擷取到異常資訊進行處理。
- UseStatusCodePages中間件相對于前兩種中間件最為簡單,其核心思路就是擷取執行完成後的Http狀态碼判斷是否是4xx 5xx相關,如果是則執行自定義的狀态碼攔截終結點。這個中間件核心是圍繞StatusCode其實并不包含處理異常相關的邏輯,是以整體實作相對簡單。
最後我們再來總結下使用中間件的方式和使用IExceptionFilter的方式的差別
- 中間件的方式是對整個請求執行管道進行異常捕獲,主要是負責整個請求過程中的異常捕獲,其生命周期更靠前,捕獲異常的範圍更廣泛。畢竟MVC隻是Asp.Net Core終結點的一種實作方式,目前Asp.Net Core還可以處理GRPC Signalr等其它類型的終結點資訊。
- IExceptionFilter主要是針對Action執行過程中的異常,畢竟終結點隻是中間件的一種形式,是以可處理範圍比較有限,主要适用于MVC程式。對于其它終結點類型有點無能為力。
以上就是文章的全部内容,由于能力有限,如果存在了解不周之處請多多諒解。我覺得學習一個東西,如果你能了解到它的工作方式或者實作原理,肯定會對你的程式設計思路有所提升,看過的代碼用過的東西可能會忘,但是思路一旦形成,将會改變你以後的思維方式。
👇歡迎掃碼關注我的公衆号👇