.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中的方法。

 

.net core 源码分析

posted @ 2024-08-09 10:07  Hi同学  阅读(42)  评论(0编辑  收藏  举报