---恢復內容開始---
筆者從事netcore相關項目開發已經大半年了,從netcore 1.0到現在3.0大概經過了3年左右的時間,記得netcore剛出來的時候國內相關的學習資料缺乏,限制於外語不大熟練的限制國外的相關書籍看起來相當吃力,於是在當當網上買了一本價值70多的入門書籍,買回來卻發現內容都是掛羊頭賣狗肉,深深地鄙視這些為了賺錢不顧內容的作者。如今網上相關的學習資料也相當多,筆者也趁着現在不忙,再來學習一下aspnetcore的源碼,文章中所用的源碼版本是3.0,如果讀者下的源碼是3.0以下,有些函數會有所區別。
下圖是筆者整理的一個簡單類圖,以助自己理解源碼。
對象介紹
WebHostBuilder:負責初始化環境變量,默認設置,指定startup類,創建servicecollection,讀取configuration,創建基礎服務並注入到DI,加載主機配置(hostingStartup),最主要的創建webhost。
WebHost:站點主機,加載應用服務,加載應用中間件,開始和停止站點
WebHostBuilderContext: 上下文,包含環境變量和默認值
WebHostOptions: 創建webhost的時候使用的參數
ServiceCollection: 所有服務的儲存的集合,添加刪除服務
serviceprovider: 獲得服務的實例
serviceDescriptor: 服務的描述類,所有的服務最后都是轉化成該類后注入DI
關鍵的對象介紹完了,下面我們來看一下web站點是如何運行起來的。
- main函數中創建WebHostBuilder對象
- 指定startup
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) { var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name; hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); // Light up the GenericWebHostBuilder implementation if (hostBuilder is ISupportsStartup supportsStartup) { return supportsStartup.UseStartup(startupType); } return hostBuilder .ConfigureServices(services => { if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) { services.AddSingleton(typeof(IStartup), startupType); } else { services.AddSingleton(typeof(IStartup), sp => { var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>(); return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)); }); } }); }
從代碼可以看到,如果這個startup是繼承自IStartup,直接注入到DI,如果不是,則用ConventtionBaseStartup進行包裝,而該類是繼承自IStartup。微軟默認的startup類不繼承IStartup的,具體的實現細節和原因在后面會具體說明。
- 加載系統的默認的配置文件,可以通過 ConfigureAppConfiguration擴展方法將自己的配置文件加載到容器里面
public static IWebHostBuilder ConfigureAppConfiguration(this IWebHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate) { return hostBuilder.ConfigureAppConfiguration((context, builder) => configureDelegate(builder)); }
- 創建WebHostBuild上下文
- 加載web主機配置,尋找程序集中貼有HostingStartupAttribute標簽的類並調用Configure方法,在WebHost實例化之前預留的鈎子,由於筆者也沒有用到過這個功能,就不多贅述了。
foreach (var assemblyName in _options.GetFinalHostingStartupAssemblies().Distinct(StringComparer.OrdinalIgnoreCase)) { try { var assembly = Assembly.Load(new AssemblyName(assemblyName)); foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>()) { var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType); hostingStartup.Configure(this); } } catch (Exception ex) { ... } }
- 創建servicecollection 類,此類存在於整個webhost生命周期,所有的應用服務和系統服務都存儲在該類中
- 添加系統服務,包括環境變量,日志服務,配置服務,監聽服務等等
var services = new ServiceCollection(); services.AddSingleton(_options); services.AddSingleton<IWebHostEnvironment>(_hostingEnvironment); services.AddSingleton<IHostEnvironment>(_hostingEnvironment); #pragma warning disable CS0618 // Type or member is obsolete services.AddSingleton<AspNetCore.Hosting.IHostingEnvironment>(_hostingEnvironment); services.AddSingleton<Extensions.Hosting.IHostingEnvironment>(_hostingEnvironment); #pragma warning restore CS0618 // Type or member is obsolete services.AddSingleton(_context); var builder = new ConfigurationBuilder() .SetBasePath(_hostingEnvironment.ContentRootPath) .AddConfiguration(_config); _configureAppConfigurationBuilder?.Invoke(_context, builder); var configuration = builder.Build(); services.AddSingleton<IConfiguration>(configuration); _context.Configuration = configuration; var listener = new DiagnosticListener("Microsoft.AspNetCore"); services.AddSingleton<DiagnosticListener>(listener); services.AddSingleton<DiagnosticSource>(listener); services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>(); services.AddTransient<IHttpContextFactory, DefaultHttpContextFactory>(); services.AddScoped<IMiddlewareFactory, MiddlewareFactory>(); services.AddOptions(); services.AddLogging(); services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();
- 創建 ServiceProvider 類,該類依賴於ServiceCollection,並為其中的服務提供實例
- 創建使用 ServiceProvider和 ServiceCollection類創建 webhost實例並初始化實例
var host = new WebHost( applicationServices, hostingServiceProvider, _options, _config, hostingStartupErrors); try { host.Initialize(); ... return host; }
初始化實例的時候會調用EnsureApplicationServices的方法。看到_startup.ConfigureServices了沒有,對這個就是我們在startup中寫的方法。
private void EnsureApplicationServices() { if (_applicationServices == null) { EnsureStartup(); _applicationServices = _startup.ConfigureServices(_applicationServiceCollection); } }
可是他是如何找到這個方法的呢,再看一下EnsureStartup方法,原來是實例化了我們在存在servicecollection中的startup類,前面我們說過,這個類ConventionBasedStartup封裝了,所以這里獲取的也ConventionBasedStartup類的實例。
private void EnsureStartup() { if (_startup != null) { return; } _startup = _hostingServiceProvider.GetService<IStartup>(); ... }
再讓我們看下ConventionBasedStartup的結構,實際上就是將該類作為一個代理來調用我們startup類中的方法
public class ConventionBasedStartup : IStartup { private readonly StartupMethods _methods; public ConventionBasedStartup(StartupMethods methods) { _methods = methods; } public void Configure(IApplicationBuilder app) { try { _methods.ConfigureDelegate(app); } catch (Exception ex) { if (ex is TargetInvocationException) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } throw; } } public IServiceProvider ConfigureServices(IServiceCollection services) { try { return _methods.ConfigureServicesDelegate(services); } catch (Exception ex) { if (ex is TargetInvocationException) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } throw; } } }
- 主機實例已創建並且相關服務已初始化完成,這時候就可以start()了。可能你會想可是我的中間件還沒加載,是的 ,start的時候就是加載中間件並創建監聽,讓我們來看一下代碼
public virtual async Task StartAsync(CancellationToken cancellationToken = default) { ... var application = BuildApplication(); ... }
private RequestDelegate BuildApplication() { try { _applicationServicesException?.Throw(); EnsureServer(); var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>(); var builder = builderFactory.CreateBuilder(Server.Features); builder.ApplicationServices = _applicationServices;
Action<IApplicationBuilder> configure = _startup.Configure; ... configure(builder); return builder.Build(); } }
發現代碼中的_startup.Configure了嗎!這個就是調用ConventionBasedStartup這個代理類的c方法,也就是我們startup中的Configure方法。這時候筆者當時也產生了一個疑問,這里的委托只有一個參數,可是在實際應用的時候都會加入很多參數,想這樣public void Configure(IApplicationBuilder app, IHostingEnvironment env,IXxxx xxxx)。猜想是不是初始化ConventionBasedStartup類的時候做了什么封裝,重新回到UseStartup方法
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName) { var configureMethod = FindConfigureDelegate(startupType, environmentName); var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName); var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName); object instance = null; if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic)) { instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType); } // The type of the TContainerBuilder. If there is no ConfigureContainer method we can just use object as it's not // going to be used for anything. var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object); var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance( typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type), hostingServiceProvider, servicesMethod, configureContainerMethod, instance); return new StartupMethods(instance, configureMethod.Build(instance), builder.Build()); }
哈哈果然 LoadMethods方法將我們寫在StartUp中的方法進行了封裝並創建了一個新的委托,到此我們也就明白為什么官方推薦的startup類不是繼承了Istartup接口的,繼承了接口的類在調用時沒法將DI中的類注入到方法參數中去,需要自己去獲取DI中的實例,筆者設想control類中的注入也是同樣的思想實現的。現在我們的所有中間件和服務已經全部加載進入WebHost中了。