ASP.NET Core 沉思录 - ServiceProvider 的二度出生
ASP.NET Core 終于將幾乎所有的對象創建工作都和依賴注入框架集成了起來。并對大部分的日常工作進行了抽象。使得整個框架擴展更加方便。各個部分的集成也更加容易。今天我們要思考的部分仍然是從一段每一個工程中都大同小異的代碼開始的。
IWebHostBuilder?CreateWebHostBuilder(string[]?args){
????return?new?WebHostBuilder()
????????.UseKestrel(ko?=>?ko.AddServerHeader?=?false)
????????.ConfigureAppConfiguration(cb?=>?cb.AddCommandLine(args))
????????.ConfigureLogging(lb?=>?{...})
????????.UseStartup<Startup>();
}
0 太長不讀
ASP.NET Core 的初始化包含了兩個步驟:第一個步驟是 Hosting 相關服務的初始化過程,初始化完畢之后創建了第一個 IServiceProvider 對象;第二步是 Application 相關服務的初始化過程。而 Application 的初始化過程可以注入 Hosting 相關的服務。之后,通過 IStartup.ConfigureServices 方法創建了第二個 IServiceProvider 對象。
初始化過程中創建的兩個 IServiceProvider 均會跟隨 WebHost 的銷毀而銷毀。
通過 Startup 類型的構造函數注入的實例是由 Hosting 初始化階段創建的 IServiceProvider 創建的。只能注入 Hosting 初始化階段添加的類型。且最好不要使用大量消耗資源的類型。
可以在 Startup.Configure 方法中添加其他參數,這樣會使用 Application 的一個 Scope 下的 IServiceProvider 進行注入,且在方法調用完畢之后該 Scope 即被銷毀。因此該方法內可以創建資源占用量較高的需要 Dispose 的類型實例而不造成泄露。
1 WebHost 的構建主要就是向 `IServiceCollection` 中添加服務
之前提到過,任何 Framework 只有兩件事情,第一件事情就是對象怎么創建,第二件事情就是如何將這些創建出來的對象塞到 Framework 處理流水線中。因此 ASP.NET Core 也是這樣。在應用程序啟動的時候,我們會在 WebHostBuilder.Build 方法調用之前進行各種各樣的操作,雖然我們調用的大部分操作都是擴展方法(例如上述代碼中的 UseXxx,和 ConfigureLogging),但是歸根結底會調用 IWebHostBuilder 的以下方法:
IWebHostBuilder?ConfigureAppConfiguration(Action<WebHostBuilderContext,?IConfigurationBuilder>?configureDelegate);IWebHostBuilder?ConfigureServices(Action<IServiceCollection>?configureServices);
IWebHostBuilder?ConfigureServices(Action<WebHostBuilderContext,?IServiceCollection>?configureServices);
不論調哪一個方法,它們做的事情其實都是一件。就是告訴應用程序,我到底有哪些對象需要創建,如何創建這些對象,以及其生存期如何管理。從技術角度上來說,就是將需要創建的對象類型添加到 IServiceCollection 中。如果感興趣的同學可以看看 WebHostBuilder 的實現代碼(https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/WebHostBuilder.cs),就更加清晰了。
例如,以 ConfigureLogging 為例,代碼請參見這里(https://github.com/aspnet/Extensions/blob/master/src/Logging/Logging/src/LoggingServiceCollectionExtensions.cs):
public?static?IWebHostBuilder?ConfigureLogging(????this?IWebHostBuilder?hostBuilder,?Action<WebHostBuilderContext,?
????ILoggingBuilder>?configureLogging)
{
????return?hostBuilder.ConfigureServices((context,?collection)?=>?
????????collection.AddLogging(builder?=>?configureLogging(context,?builder)));
}
public?static?IServiceCollection?AddLogging(
????this?IServiceCollection?services,?
????Action<ILoggingBuilder>?configure)
{
????if?(services?==?null)?{?throw?new?ArgumentNullException(nameof(services));?}
????services.AddOptions();
????services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory,?LoggerFactory>());
????services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>),?typeof(Logger<>)));
????services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
????????new?DefaultLoggerLevelConfigureOptions(LogLevel.Information)));
????configure(new?LoggingBuilder(services));
????return?services;
}
可以看到實際上就是將 IOptions<>、IOptionsSnapshot<>、IOptionsMonitor<>、IOptionsFactory<>、IOptionsMonitorCache<> 以及 ILoggerFactory、ILogger<>、IConfigureOptions<LoggerFilterOptions> 添加到 IServiceCollection 中的過程。有關日志的內容我們會在另一篇文章中介紹。
2 Startup 初始化時為什么又能注入又有 `IServiceCollection` 呢
在 WebHost 的構建過程中,十有八九會出現 UseStartup 這句話(如果不出現這句話,那么很大程度上使用了 Configure 擴展方法)。Startup 是整個 Web 應用程序的起點。應用程序(Web App)托管在宿主(Hosting Environment)中。那么它應當是在初始化的最終階段執行的。我們來觀察一下它的典型結構:
public?class?Startup{
????public?void?ConfigureServices(IServiceCollection?services)
????{
????????//?Add?application?related?services?to?service?collection.
????}
????public?void?Configure(IApplicationBuilder?app,?IHostingEnvironment?env)
????{
????????//?Create?application?pipeline.?We?will?not?focus?on?this?method.
????}
}
如果單純觀察上述代碼那么并沒有任何的稀奇之處。ConfigureServices 方法將應用需要的類型全部添加到 IServiceCollection 實例中,而 Configure 來構建 Pipeline(我們此次不討論該方法)。但是如果我們需要記錄日志,讀取配置文件,在應用程序生命周期事件中注冊新的處理方法時,我們可以將其直接注入 Startup 中。例如:
public?class?Startup{
????readonly?IConfiguration?configuration;
????readonly?IApplicationLifetime?lifetime;
????readonly?ILogger<Startup>?logger;
????public?Startup(
????????IConfiguration?configuration,?IApplicationLifetime?lifetime,?ILogger<Startup>?logger)
????{
????????this.configuration?=?configuration;
????????this.lifetime?=?lifetime;
????????this.logger?=?logger;
????}
????public?void?ConfigureServices(IServiceCollection?services)
????{
????????//?Add?application?related?services?to?service?collection.
????}
????public?void?Configure(IApplicationBuilder?app,?IHostingEnvironment?env)
????{
????????//?Create?application?pipeline.
????}
}
那么問題就來了。
在 Startup 中注入的 configuration、lifetime、logger 這些服務是由哪一個 IServiceProvider 創建出來的呢?
如果在 Startup 創建時 IServiceProvider 已然創建,那么 Startup.ConfigureServices 在向哪個 IServiceCollection 實例添加類型呢?
應用程序運行期間的 IServiceProvider 是在 Startup 創建之前就創建好的那個呢、還是由 Startup 配置的 IServiceCollection 實例創建的那個呢?
3 兩階段 ServiceProvider 創建
既然 Startup 中已經有一個 IServiceProvider 來給相應的類型進行依賴注入,而平時的應用程序中的依賴注入又能夠包含 Startup.ConfigureServices 中的類型定義,那么說明在整個初始化過程中先后創建了兩個 IServiceProvider 對象。
即 ASP.NET Core 的初始化包含了兩個步驟:
第一個步驟是 Hosting 相關服務的初始化過程,初始化完畢之后創建了第一個 IServiceProvider 對象;
第二步是 Application 相關服務的初始化過程。而 Application 的初始化過程可以注入 Hosting 相關的服務。之后,通過 IStartup.ConfigureServices 方法創建了第二個 IServiceProvider 對象。
如果你對源代碼感興趣
請參考 WebHostBuilder 類的 Build 方法(源代碼在這里:https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/WebHostBuilder.cs)。大致的過程如下:
BuildCommonServices 方法將所有 Hosting 所需的服務(WebHost 相關類型以及所有 IWebHostBuilder 調用中添加的服務類型)添加到 IServiceCollection 對象中。
使用該 IServiceCollection 創建 Hosting 相關的 IServiceProvider,不妨稱之為 hostingServiceProvider。
使用該 hostingServiceProvider 創建 IStartup 對象(這里有和環境相關的 Convension,詳情請參見上一篇)。
使用一個復制的 IServiceCollection 對象調用 IStartup.ConfigureServices 方法創建另外一個 IServiceProvider 不妨稱之為 applicationServiceProvider。
在了解了上述過程之后,那么我們需要注意些什么呢?
首先我們已經了解,Startup 可以使用 Hosting 的 IServiceProvider 進行注入。但是 IServiceProvider 是一個頂級的 Provider,如果我們在 Startup 中創建了一個非常消耗資源的對象(實現了 IDisposable),則在默認情況下該對象只有在應用程序徹底退出的時候才會銷毀。若顯式 Dispose 該對象的話且該對象不是 Transient Scope。則有可能導致 Defect。
4 規避初始化過程中的資源泄露
但是如果我真的需要在初始化的時候注入非常消耗資源的對象,而我又希望規避資源的泄露,我該怎么辦呢?其實還是有辦法的。那就是不使用 Startup 的構造函數進行注入而是直接在 Configure 方法中通過參數進行注入。
為什么這種方式可以規避資源泄露呢?因為這種注入機智并非典型的依賴注入機制,而是 ASP.NET Core 特意實現的。如果應用程序在初始化時使用的 UseStartup<TStartup>() 中的 TStartup 并沒有實現 IStartup 的話,ASP.NET Core 就會使用基于約定的 IStartup 實現對 TStartup 進行包裝。在包裝過程中,它會嘗試找到 TStartup 類型中的 Configure 方法,檢查參數表中的參數,并使用 IStartup.ConfigureServices 創建的 IServiceProvider 進行注入。但是這里的 IServiceProvider 卻并不初始化過程中的頂級 Provider。而是在將整個方法調用包裹在了 Scope 里。因此即使在初始化過程中創建非常消耗資源的實例也會隨著方法調用結束后 Scope 的 Dispose 而銷毀。具體代碼請參見:ConfigureBuilder 源代碼 (https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs)
5 總結
請飛到文章開頭的第 0 節 :-D。
如果您覺得本文對您有幫助,也歡迎分享給其他的人。我們一起進步。歡迎關注我的博客(https://clrdaily.com)和微信公眾號:
總結
以上是生活随笔為你收集整理的ASP.NET Core 沉思录 - ServiceProvider 的二度出生的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 现身说法:实际业务出发分析百亿数据量下的
- 下一篇: 微软开源故事 | 开启 .NET 开源革