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