.net core 源码分析(2) 启动过程-Startup
在.net core web 启动过程(1)中介绍了IHostStartup的执行过程,该文章主要介绍IStartup的执行过程。
最常用的配置Startup方式,通过调用webHostBuilder扩展方法UseStartup<T> 来指定。
var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => { webHostBuilder .UseConfiguration(config) .UseKestrel() .UseStartup<StartupBlockingOnStart>(); }) .Build();
这里看一下WebHostBuilder.UseStartup<T>扩展方法的具体实现
/// <summary> /// Specify the startup type to be used by the web host. /// </summary> /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> /// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam> /// <returns>The <see cref="IWebHostBuilder"/>.</returns> public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class { return hostBuilder.UseStartup(typeof(TStartup)); }
internal sealed class GenericWebHostBuilder : WebHostBuilderBase, ISupportsStartup { private object? _startupObject; private readonly object _startupKey = new object(); private AggregateException? _hostingStartupErrors; private HostingStartupWebHostBuilder? _hostingStartupWebHostBuilder; public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType) { //这里得到指定的startup类型所在的Assembly名称 var startupAssemblyName = startupType.Assembly.GetName().Name; //记录在设置中,多次执行会覆盖上一次的 UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); //UseStartup 允许多次调用,但是只会允许最后一次,这里和IHostStartup的机制不同,IHostStartup允许多个实现 // UseStartup can be called multiple times. Only run the last one. //_startupObject是object类型,他是GenericWebHostBuilder对象中的一个私有变量,多次调用UseStartup,_startObject会被覆盖为最后一次的设置对象。 _startupObject = startupType; //重点关注,UseStartup这里会把具体操作给 转化成一个Action给暂存到ConfigureSercices。最终会在HostBuilder.Build()的InitializeServiceProvider()中执行,
_builder.ConfigureServices((context, services) => { // Run this delegate if the startup type matches //多次调用UseStartup,暂存多个Action,但是在执行的时候,这里会有一个验证,仅最后一次设置的才能通过, if (object.ReferenceEquals(_startupObject, startupType)) { UseStartup(startupType, context, services); } }); return this; } }
看完上述代码后,需要提注意的是,虽然UseStartup具体操作被暂存了起来,但是 startupAssemblyName 和_startupObject是被立即记录起来的。
配置Startup的其他方式,通过传入startupFactory来实现动态改变startup的目的
/// <summary> /// Specify a factory that creates the startup instance to be used by the web host. /// </summary> /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param> /// <param name="startupFactory">A delegate that specifies a factory for the startup class.</param> /// <returns>The <see cref="IWebHostBuilder"/>.</returns> /// <remarks>When in a trimmed app, all public methods of <typeparamref name="TStartup"/> are preserved. This should match the Startup type directly (and not a base type).</remarks> public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TStartup>(this IWebHostBuilder hostBuilder, Func<WebHostBuilderContext, TStartup> startupFactory) where TStartup : class { //通过传入一个Func<WebHostBuilderContext,TStartup>的startupFactory来实现动态改变Startup对象 }
// Note: This method isn't 100% compatible with trimming. It is possible for the factory to return a derived type from TStartup. // RequiresUnreferencedCode isn't on this method because the majority of people won't do that. public IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TStartup>(Func<WebHostBuilderContext, TStartup> startupFactory) { //这里和useStartup(Type t)一样,都是记录startup所在的程序集 var startupAssemblyName = startupFactory.GetMethodInfo().DeclaringType!.Assembly.GetName().Name; UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); //覆盖上次设置类型 // Clear the startup type _startupObject = startupFactory; //最终在InitializeServiceProvider()阶段执行ConfigStartup方法 _builder.ConfigureServices(ConfigureStartup); [UnconditionalSuppressMessage("Trimmer", "IL2072", Justification = "Startup type created by factory can't be determined statically.")] void ConfigureStartup(HostBuilderContext context, IServiceCollection services) { // UseStartup can be called multiple times. Only run the last one. if (object.ReferenceEquals(_startupObject, startupFactory)) { var webHostBuilderContext = GetWebHostBuilderContext(context); var instance = startupFactory(webHostBuilderContext) ?? throw new InvalidOperationException("The specified factory returned null startup instance."); UseStartup(instance.GetType(), context, services, instance); } } return this; }
对比两种配置方式,发现内部逻辑基本一致,最终都会暂存一个Action到HostBuilder.ConfigureServices,该action 都会有一个拦截验证,只允许最后一次配置startup的方式允许通过。 两种方式暂存的action 里面的操作都最终指向
private void UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType, HostBuilderContext context, IServiceCollection services, object? instance = null)
那我们来看看,他们暂存的action 到具体执行的时候都做了什么事。
private void UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType, HostBuilderContext context, IServiceCollection services, object? instance = null) { var webHostBuilderContext = GetWebHostBuilderContext(context); var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)]; ExceptionDispatchInfo? startupError = null; ConfigureBuilder? configureBuilder = null; try { //我们UseStartup<T>和StartupFactory 他们指定的Startup具体类是不能继承IStartup接口,因为IStartup接口的ConfigureServices方法需要返回ServiceProvider //指定的Startup具体类的ConfigureServices是无需返回值的 // We cannot support methods that return IServiceProvider as that is terminal and we need ConfigureServices to compose if (typeof(IStartup).IsAssignableFrom(startupType)) { throw new NotSupportedException($"{typeof(IStartup)} isn't supported"); } if (StartupLoader.HasConfigureServicesIServiceProviderDelegate(startupType, context.HostingEnvironment.EnvironmentName)) { throw new NotSupportedException($"ConfigureServices returning an {typeof(IServiceProvider)} isn't supported."); } //如果使用的UseStartup<T>(Type type)这里需要创建对象, //如果使用的startupFactory instance是不等null的,具体可以看一下startupFactory那里的具体实现,暂存的action执行过程中会通过startupFactory创建指定的对象 instance ??= ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType); context.Properties[_startupKey] = instance; // Startup.ConfigureServices var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName); var configureServices = configureServicesBuilder.Build(instance); //这里执行执行Startup.ConfigureServices方法 configureServices(services); // REVIEW: We're doing this in the callback so that we have access to the hosting environment // Startup.ConfigureContainer在这里也不会被执行 var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName); if (configureContainerBuilder.MethodInfo != null) { // Store the builder in the property bag _builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder; InvokeContainer(this, configureContainerBuilder); } // Resolve Configure after calling ConfigureServices and ConfigureContainer configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName); } catch (Exception ex) when (webHostOptions.CaptureStartupErrors) { startupError = ExceptionDispatchInfo.Capture(ex); } // Startup.Configure 这里并不会被执行,这里通过依赖注入把执行Startup.Configure方法给绑定到了GenericWevHostServiceOptions中,后边可以通过获取该options对象来执行 services.Configure<GenericWebHostServiceOptions>(options => { options.ConfigureApplication = app => { // Throw if there was any errors initializing startup startupError?.Throw(); //执行 Startup.Configure // Execute Startup.Configure if (instance != null && configureBuilder != null) { configureBuilder.Build(instance)(app); } }; });
//关于依赖注入的功能在本章不过多讲述,后边会单独出一篇相关文章详细介绍,这里只要了解一下即可。
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "There is a runtime check for ValueType startup container. It's unlikely anyone will use a ValueType here.")] static void InvokeContainer(GenericWebHostBuilder genericWebHostBuilder, ConfigureContainerBuilder configureContainerBuilder) { // 得到Startup.ConfigureContainer(TContainerBuilder containerBuilder)的具体类型,在写Startup类时候,这个方法的参数TContainerBuilder的类型是可以修改成其他类型的, var containerType = configureContainerBuilder.GetContainerType(); // 如果容器类型是值类型且当前环境不支持动态代码生成,则抛出异常 if (containerType.IsValueType && !RuntimeFeature.IsDynamicCodeSupported) { throw new InvalidOperationException("A ValueType TContainerBuilder isn't supported with AOT."); } // 构建 Action<HostBuilderContext, TContainerBuilder> 委托类型,TContrainerBuilder是上面获取到的contarinerType var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType); // 获取 GenericWebHostBuilder 类中的私有方法 ConfigureContainerImpl,并创建委托,动态创建了一个类型为Action<HostBuilderContext,TContainerBuilder>的变量 var configureCallback = typeof(GenericWebHostBuilder).GetMethod(nameof(ConfigureContainerImpl), BindingFlags.NonPublic | BindingFlags.Instance)! .MakeGenericMethod(containerType) .CreateDelegate(actionType, genericWebHostBuilder); // 通过反射调用 IHostBuilder.ConfigureContainer(Action) 方法,把传入的action 给暂存到 _configureContainerActions中, 在IHostBuilder.Build()的 InitializeServiceProvider阶段中统一执行 typeof(IHostBuilder).GetMethod(nameof(IHostBuilder.ConfigureContainer))! .MakeGenericMethod(containerType) .InvokeWithoutWrappingExceptions(genericWebHostBuilder._builder, new object[] { configureCallback }); } }
根据上面代码中可以看出Startup类中方法的执行顺序
1. void ConfigureServices(IServiceCollection services) ;
2. void ConfigureContainer(ContainerBuilder builder);
3. void Configure(IApplicationBuilder app, IWebHostEnvironment env);
其中 1和2 在InitializeServiceProvider()阶段被执行,但是3却并没有在这里执行。
关于依赖注入这部分会在后期详细讲述,这里简单介绍一下Startup几个方法的执行顺序。
看到 Startup.Configure的参数和IHostStartup.Configure区别了吗
1. IHostStartup.Configure(IWebHostBuilder builder) 用于配置WebHostBuilder ,
2. Starup.Configure(IApplicationBuilder app, IWebHostEnvironment env)用于配置ApplicationBuilder ,配置HTTP 请求管道的中间件。
IHostStartup.Configure允许多个实现,是在HostBuilder的Build()方法中 InitializeHostConfiguration()阶段被执行 ,Starup允许多次UseStartup ,但是只允许执行最后一个Startup的方法,
Starup.Configure是在HostBuilder的Build()方法中InitializeServiceProvider()阶段被执行。
讲完上面,下面给出一个常见.net core web StartUp类
public class Startup { // ConfigureServices public void ConfigureServices(IServiceCollection services) { services.AddControllers(); } //ConfigureContainer public void ConfigureContainer(ContainerBuilder builder) { } // Configure 是用于配置 HTTP 请求管道的地方 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
写到这里总感觉对于启动过程总是缺少了什么,比如说ServiceConllection是如何创建的,是什么时候创建的 。Startup.ConfigureContainer是做什么。他们直接是如何在上层对象中调用的。
想讲明白启动过程还需要了解一下HostBuilder中的方法。