文章目錄
- 中間件:掌控請求處理過程的關鍵
- 1. 中間件
- 1.1 中間件工作原理
- 1.2 中間件核心對象
- 2.異常處理中間件:區分真異常和邏輯異常
- 2.1 處理異常的方式
- 2.1.1 日常錯誤處理--定義錯誤頁的方法
- 2.1.2 使用代理方法處理異常
- 2.1.3 異常過濾器 IExceptionFilter
- 2.1.4 特性過濾 ExceptionFilterAttribute
- 2.1.5 異常處理技巧總結
- 3 靜態文件中間件: 前后端分離開發合并部署
- 3.1 靜態文件中間件的能力
- 3.2 注冊使用非www.root目錄
- 4 文件提供程序:將文件放在任何地方
- 4.1 文件提供程序核心類型
- 4.2 內置文件提供程序
中間件:掌控請求處理過程的關鍵
1. 中間件
1.1 中間件工作原理
1.2 中間件核心對象
- IApplicationBuilder
- RequestDelegate
IApplicationBuilder可以通過委托方式注冊中間件,委托的入參也是委托,這就可以將這些委托注冊成一個鏈,如上圖所示;最終會調用Builder方法返回一個委托,這個委托就是把所有的中間件串起來后合并成的一個委托方法,Builder的委托入參是HttpContext(實際上所有的委托都是對HttpContext進行處理);
RequestDelegate是處理整個請求的委托。
中間件的執行順序和注冊順序是相關的
//注冊委托方式,注冊自己邏輯// 對所有請求路徑app.Use(async (context, next) =>{await next();await context.Response.WriteAsync("Hello2");});// 對特定路徑指定中間件,對/abc路徑進行中間件注冊處理app.Map("/abc", abcBuilder =>{// Use表示注冊一個完整的中間件,將next也注冊進去abcBuilder.Use(async (context, next) =>{await next();await context.Response.WriteAsync("abcHello");});});// Map復雜判斷,判斷當前請求是否符合某種條件app.MapWhen(context =>{return context.Request.Query.Keys.Contains("abc");}, builder =>{// 使用Run表示這里就是中間件的執行末端,不再執行后續中間件builder.Run(async context =>{await context.Response.WriteAsync("new abc");});});
應用程序一旦開始向Response進行write時,后續的中間件就不能再操作Head,否則會報錯
可以通過context.Resopnse.HasStarted方法判斷head是否已經被操作
- 設計自己的中間件
中間件的設計時才有的約定的方式,即在方法中包含Invoke或者InvokeAsync,如下:
public class MyMiddleware{private readonly RequestDelegate next;private readonly ILogger<MyMiddleware> logger;public MyMiddleware(RequestDelegate next,ILogger<MyMiddleware> logger){this.next = next;this.logger = logger;}public async Task InvokeAsync(HttpContext context){using (logger.BeginScope("TraceIndentifier:{TraceIdentifier}",context.TraceIdentifier)){logger.LogDebug("Start");await next(context);logger.LogDebug("End");}}}public static class MyBuilderExtensions
{public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app){return app.UseMiddleware<MyMiddleware>();}
}// 中間件使用
app.UseMyMiddleware();
2.異常處理中間件:區分真異常和邏輯異常
2.1 處理異常的方式
- 異常處理頁
- 異常處理匿名委托方法
- IExceptionFilter
- ExceptionFilterAttribute
// startup中的Configureif (env.IsDevelopment()){app.UseDeveloperExceptionPage();// 開發環境下的異常頁面,生產環境下是需要被關閉,頁面如下圖所示app.UseSwagger();app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LoggingSerilog v1"));
}
2.1.1 日常錯誤處理–定義錯誤頁的方法
// startup中的Configure
app.UseExceptionHandler("/error");// 控制器
public class ErrprController : Controller
{[Route("/error")]public IActionResult Index(){// 獲取上下文中的異常var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();var ex = exceptionHandlerPathFeature?.Error;var knowException = ex as IKnowException;if(knowException == null){var logger = HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();logger.LogError(ex,ex.Message);knowException = KnowException.Unknow;}else{knowException = KnowException.FromKnowException(knowException);}return View(knowException);}
}// 定義接口
public interface IKnowException
{public string Message {get;}public int ErrorCode {get;}public object[] ErrorData {get;}
}// 定義實現
public class KnowException : IKnowException
{public string Message {get;private set;}public int ErrorCode {get;private set;}public object[] ErrorData {get;private set;}public readonly static IKnowException Uknow = new KnowException{Message = "未知錯誤",ErrorCode = 9999};public static IKnowException FromKnowException(IKnowException exception){return new KnowException{Message = exception.Message,ErrorCode = exception.ErrorCode,ErrorData = exception.ErrorData};}
}// 需要定義一個錯誤頁面 index.html,輸出錯誤Message和ErrorCode
2.1.2 使用代理方法處理異常
// startup中的Configure
app.UseExceptionHandler(errApp =>
{errApp.Run(async context =>{var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();var ex = exceptionHandlerPathFeature?.Error;var knowException = ex as IKnowException;if(knowException == null){var logger = HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();logger.LogError(exceptionHandlerPathFeature.Error,exceptionHandlerPathFeature.Error.Message);knowException = KnowException.Unknow;context.Response.StatusCode = StatusCodes.Status500InternalServerError;}else{knowException = KnowException.FromKnowException(knowException);context.Response.StatusCode = StatusCodes.Status200OK;}var jsonOptions = context.RequestServices.GetService<Options<JsonOptions>>();context.Response.ContextType = "application/json";charset=utf-8";await context.Response.WriteAsync(System.Text.Json.JsonSerializer(knowException,jsonOptions.Value));});
});
- 未知異常輸出Http500響應,已知異常輸出Http200
因為監控系統會對Http響應碼進行識別,如果返回的500比率比較高的時候,會認為系統的可用性有問題,告警系統會發出警告。對已知異常進行200響應能夠讓告警系統正常運行,能夠正確識別系統一些未知的錯誤,使告警系統更加靈敏,避免了業務邏輯的異常干擾告警系統
2.1.3 異常過濾器 IExceptionFilter
異常過濾器是作用在整個MVC框架體系之下,在MVC整個聲明周期中發生作用,也就是說它只能工作早MVC Web Api的請求周期里面
// 自定義異常過濾器
public class MyException : IExceptionFilter
{public void OnException(ExceptionContext context){IKnowException knowException = context.Exception as IKnowException;if(knowException == null){var loger = context.HttpContext.RequestServices.GetService<ILogger<MyExceptionFilterAttribute>>();logger.LogError(context.Exception,context.Exception.Message);knowException = KnowException.UnKnow;context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;}else{knowException = KnowException.FromKnowException(knowException);context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;}context.Result = new JsonResult(knowException){ContextType = "application/json:charset=utf-8"}}
}// startup注冊
public void ConfigureServices(IServiceCollection services)
{services.AddMvc(mvcoption => {mvcOptions.Filters.Add<MyExceptionFilter>();}).AddJsonOptions(jsonOptions => {jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscapt});
}
2.1.4 特性過濾 ExceptionFilterAttribute
public class MyExceptionFilterAttriburte : ExceptionFilterAttribute
{public override void OnException(ExceptionContext context){IKnowException knowException = context.Exception as IKnowException;if(knowException == null){var logger = context.HttpContext.RequestServices.GetServices<ILogger<MyExceptionFilterAttribute>>();logger.LogError(context.Exception,context.Exception.Message);knowException = KnowException.UnKnow;context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;}else{knowException = KnowException.FromKnowException(knowException);context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;}context.Result = new JsonResult(knowException){ContextType = "application/json:charset=utf-8"}}
}// 使用方式
在Controller控制器上方標注[MyExceptionFilter]
或者在 startup中ConfigureServices注冊
services.AddMvc(mvcoption => {mvcOptions.Filters.Add<MyExceptionFilterAttribute>();});
2.1.5 異常處理技巧總結
- 用特定的異常類或接口表示業務邏輯異常
- 為業務邏輯異常定義全局錯誤碼
- 為未知異常定義定義特定的輸出信息和錯誤碼
- 對于已知業務邏輯異常響應HTTP 200(監控系統友好)
- 對于未預見的異常響應HTTP 500
- 為所有的異常記錄詳細的日志
3 靜態文件中間件: 前后端分離開發合并部署
3.1 靜態文件中間件的能力
- 支持指定相對路徑
- 支持目錄瀏覽
- 支持設置默認文檔
- 支持多目錄映射
// startup的Configure方法中
app.UseDefaultFiles();// 設置默認訪問根目錄文件index.html
app.UseStaticFiles();// 將www.root目錄映射出去
如果需要瀏覽文件目錄,需要如下配置
// startup中的ConfigureServices中配置
services.AddDirectoryBrowser();// startup的Configure方法中
app.UseDirectoryBrowser();
app.UseStaticFiles();
3.2 注冊使用非www.root目錄
// startup的Configure方法中
app.UseStaticFiles();app.UseStaticFiles(new StaticFileOptions
{// 將程序中名為file文件目錄注入RequestPath = "/files",// 設置文件指定訪問路徑,將文件目錄映射為指定的Url地址FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(),"file"))
});
實際生產中會遇到將非api請求重定向到指定目錄,采用如下配置
// startup的Configure方法中
app.MapWhen(context =>
{return !context.Request.Path.Value.StartWith("/api");
},appBuilder =>
{var option = new RewriteOptions();option.AddRewrite(".*","/index.html",true);appBuilder.UseRewriter(option);appBuilder.UseStaticFiles();
});
4 文件提供程序:將文件放在任何地方
4.1 文件提供程序核心類型
- IFileProvider
- IFileInfo
- IDirectoryContexts
4.2 內置文件提供程序
- PhysicalFileProvider? 物理文件提供程序
- EmbeddedFileProvider ? 嵌入式文件提供程序
- CompositeFileProvoder ? 組合文件提供程序,將各種文件程序組合成一個目錄
// 映射指定目錄文件- 物理文件
IFileProvider provider1 = new PhysicalFileProvider(AppDomain.CurrentDomain.BaseDirectory);
// 獲取文件目錄下內容
var contents = provider1.GetDirectoryContents("/");
// 輸出文件信息
foreach (var item in contents)
{var stream = item.CreateReadStream();// 獲取文件流Console.WriteLine(item.Name);
}// 嵌入式文件
IFileProvider fileProvider2 = new EmbeddedFileProvider(typeof(Program).Assembly);
var html = fileProvider2.GetFileInfo("file.html");// file.html文件屬性設置為嵌入的資源// 組合文件提供程序
IFileProvider fileProvider3 = new CompositeFileProvider(provider1,fileProvider2);
var contexts3 = fileProvider3.GetDirectoryContents("/");