前言
中間件(Middleware)對於Asp.NetCore項目來說,不能說重要,而是不能缺少,因為Asp.NetCore的請求管道就是通過一系列的中間件組成的;在服務器接收到請求之后,請求會經過請求管道進行相關的過濾或處理;
正文
那中間件是那路大神?
會經常聽說,需要注冊一下中間件,如圖:
所以說,中間件是針對請求進行某種功能需求封裝的組件,而這個組件可以控制是否繼續執行下一個中間件;如上圖中的app.UserStaticFiles()就是注冊靜態文件處理的中間件,在請求管道中就會處理對應的請求,如果沒有靜態文件中間件,那就處理不了靜態文件(如html、css等);這也是Asp.NetCore與Asp.Net不一樣的地方,前者是根據需求添加對應的中間件,而后者是提前就全部准備好了,不管用不用,反正都要路過,這也是Asp.NetCore性能比較好的原因之一;
而對於中間件執行邏輯,官方有一個經典的圖:
如圖所示,請求管道由一個個中間件(Middleware)組成,每個中間件可以在請求和響應中進行相關的邏輯處理,在有需要的情況下,當前的中間件可以不傳遞到下一個中間件,從而實現斷路;如果這個不太好理解,如下圖:
每層外圈代表一個中間件,黑圈代表最終的Action方法,當請求過來時,會依次經過中間件,Action處理完成后,返回響應時也依次經過對應的中間件,而執行的順序如箭頭所示;(這里省去了一些其他邏輯,只說中間件)。
好了好了,理論說不好,擔心把看到的小伙伴繞進去了,就先到這吧,接下來從代碼中看看中間件及請求管道是如何實現的;老規矩,找不到下手的地方,就先找能"摸"的到的地方,這里就先扒靜態文件的中間件:
namespace Microsoft.AspNetCore.Builder
{
public static class StaticFileExtensions
{
// 調用就是這個擴展方法
public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// 這里調用了 IApplicationBuilder 的擴展方法
return app.UseMiddleware<StaticFileMiddleware>();
}
// 這里省略了兩個重載方法,是可以指定參數的
}
}
UseMiddleware方法實現
// 看着調用的方法
public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
{
// 內部調用了以下方法
return app.UseMiddleware(typeof(TMiddleware), args);
}
// 其實這里是對自定義中間件的注冊,這里可以不用太深入了解
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
{
if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
{
// IMiddleware doesn't support passing args directly since it's
// activated from the container
if (args.Length > 0)
{
throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
}
return UseMiddlewareInterface(app, middleware);
}
// 取得容器
var applicationServices = app.ApplicationServices;
// 反編譯進行包裝成注冊中間件的樣子(Func<ReuqestDelegate,RequestDelegate>),但可以看到本質使用IApplicationBuilder中Use方法
return app.Use(next =>
{
// 獲取指定類型中的方法列表
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
// 找出名字是Invoke或是InvokeAsync的方法
var invokeMethods = methods.Where(m =>
string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
|| string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
).ToArray();
// 如果有多個方法 ,就拋出異常,這里保證方法的唯一
if (invokeMethods.Length > 1)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
}
// 如果沒有找到,也就拋出異常
if (invokeMethods.Length == 0)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}
// 取得唯一的方法Invoke或是InvokeAsync方法
var methodInfo = invokeMethods[0];
// 判斷類型是否返回Task,如果不是就拋出異常,要求返回Task的目的是為了后續包裝RequestDelegate
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
// 判斷方法的參數,參數的第一個參數必須是HttpContext類型
var parameters = methodInfo.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}
// 開始構造RequestDelegate對象
var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
// 如果參數只有一個HttpContext 就包裝成一個RequestDelegate返回
if (parameters.Length == 1)
{
return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
}
// 如果參數有多個的情況就單獨處理,這里不詳細進去了
var factory = Compile<object>(methodInfo, parameters);
return context =>
{
var serviceProvider = context.RequestServices ?? applicationServices;
if (serviceProvider == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
return factory(instance, context, serviceProvider);
};
});
}
以上代碼其實現在拿出來有點早了,以上是對自定義中間件的注冊方式,為了扒代碼的邏輯完整,拿出來了;這里可以不用深究里面內容,知道內部調用了IApplicationBuilder的Use方法即可;
由此可見,IApplicationBuilder就是構造請求管道的核心類型,如下:
namespace Microsoft.AspNetCore.Builder
{
public interface IApplicationBuilder
{
// 容器,用於依賴注入獲取對象的
IServiceProvider ApplicationServices
{
get;
set;
}
// 屬性集合,用於中間件共享數據
IDictionary<string, object> Properties
{
get;
}
// 針對服務器的特性
IFeatureCollection ServerFeatures
{
get;
}
// 構建請求管道
RequestDelegate Build();
// 克隆實例的
IApplicationBuilder New();
// 注冊中間件
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}
}
IApplicationBuilder的默認實現就是ApplicationBuilder,走起,一探究竟:
namespace Microsoft.AspNetCore.Builder
{ // 以下 刪除一些屬性和方法,具體可以私下看具體代碼
public class ApplicationBuilder : IApplicationBuilder
{
// 存儲注冊中間件的鏈表
private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
// 注冊中間件
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
// 將中間件加入到鏈表
_components.Add(middleware);
return this;
}
// 構造請求管道
public RequestDelegate Build()
{
// 構造一個404的中間件,這就是為什么地址匹配不上時會報404的原因
RequestDelegate app = context =>
{
// 判斷是否有Endpoint中間件
var endpoint = context.GetEndpoint();
var endpointRequestDelegate = endpoint?.RequestDelegate;
if (endpointRequestDelegate != null)
{
var message =
$"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " +
$"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
$"routing.";
throw new InvalidOperationException(message);
}
// 返回404 Code
context.Response.StatusCode = 404;
return Task.CompletedTask;
};
// 構建管道,首先將注冊的鏈表倒序一把,保證按照注冊順序執行
foreach (var component in _components.Reverse())
{
app = component(app);
}
// 最終返回
return app;
}
}
}
在注冊的代碼中,可以看到所謂的中間件就是Func<RequestDelegate, RequestDelegate>,其中RequestDelegate就是一個委托,用於處理請求的,如下:
public delegate Task RequestDelegate(HttpContext context);
之所以用Func<RequestDelegate, RequestDelegate>的形式表示中間件,應該就是為了中間件間驅動方便,畢竟中間件不是單獨存在的,是需要多個中間件結合使用的;
那請求管道構造完成了,那請求是如何到管道中呢?
應該都知道,Asp.NetCore內置了IServer(如Kestrel),負責監聽對應的請求,當請求過來時,會將請求給IHttpApplication
namespace Microsoft.AspNetCore.Hosting.Server
{
public interface IHttpApplication<TContext>
{
// 執行上下文創建
TContext CreateContext(IFeatureCollection contextFeatures);
// 執行上下文釋放
void DisposeContext(TContext context, Exception exception);
// 處理請求,這里就使用了請求管道處理
Task ProcessRequestAsync(TContext context);
}
}
而對於IHttpApplication
namespace Microsoft.AspNetCore.Hosting
{
internal class HostingApplication : IHttpApplication<HostingApplication.Context>
{
// 構建出來的請求管道
private readonly RequestDelegate _application;
// 用於創建請求上下文的
private readonly IHttpContextFactory _httpContextFactory;
private readonly DefaultHttpContextFactory _defaultHttpContextFactory;
private HostingApplicationDiagnostics _diagnostics;
// 構造函數初始化變量
public HostingApplication(
RequestDelegate application,
ILogger logger,
DiagnosticListener diagnosticSource,
IHttpContextFactory httpContextFactory)
{
_application = application;
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource);
if (httpContextFactory is DefaultHttpContextFactory factory)
{
_defaultHttpContextFactory = factory;
}
else
{
_httpContextFactory = httpContextFactory;
}
}
// 創建對應的請求的上下文
public Context CreateContext(IFeatureCollection contextFeatures)
{
Context hostContext;
if (contextFeatures is IHostContextContainer<Context> container)
{
hostContext = container.HostContext;
if (hostContext is null)
{
hostContext = new Context();
container.HostContext = hostContext;
}
}
else
{
// Server doesn't support pooling, so create a new Context
hostContext = new Context();
}
HttpContext httpContext;
if (_defaultHttpContextFactory != null)
{
var defaultHttpContext = (DefaultHttpContext)hostContext.HttpContext;
if (defaultHttpContext is null)
{
httpContext = _defaultHttpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
else
{
_defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures);
httpContext = defaultHttpContext;
}
}
else
{
httpContext = _httpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
_diagnostics.BeginRequest(httpContext, hostContext);
return hostContext;
}
// 將創建出來的請求上下文交給請求管道處理
public Task ProcessRequestAsync(Context context)
{
// 請求管道處理
return _application(context.HttpContext);
}
// 以下刪除了一些代碼,具體可下面查看....
}
}
這里關於Server監聽到請求及將請求交給中間處理的具體過程沒有具體描述,可以結合啟動流程和以上內容在細扒一下流程吧(大家私下搞吧),這里就簡單說說中間件及請求管道構建的過程;(后續有時間將整體流程走一遍);
總結
這節又是純代碼來“忽悠”小伙伴了,對於理論概念可能表達的不夠清楚,歡迎交流溝通;其實這里只是根據流程走了一遍源碼,並沒有一行行解讀,所以小伙伴看此篇文章代碼部分的時候,以調試的思路去看,從注冊中間件那塊開始,到最后請求交給請求管道處理,注重這個流程即可;
下一節說說中間件的具體應用;
------------------------------------------------
一個被程序搞丑的帥小伙,關注"Code綜藝圈",識別關注跟我一起學~~~