源码解析.Net中Host主机的构建过程
前言
本篇文章着重讲一下在.Net中Host主机的构建过程,依旧延续之前文章的思路,着重讲解其源码,如果有不知道有哪些用法的同学可以点击这里,废话不多说,咱们直接进入正题
Host构建过程
下图是我自己整理的Host构建过程以及里面包含的知识点我都以链接的形式放上来,大家可以看下图,大概了解下过程(由于知识点过多,所以只能分上下两张图了😊):
图中标识的相关知识点连接如下(ps:与编号对应):
- 1 环境变量 点击这里
- 2 命令行参数 点击这里
- 3 默认配置 点击这里
- 4 用户机密数据 点击这里
- 5 默认logging 点击这里
- 6 使用承载启动程序集 点击这里
- 7 自定义配置 点击这里
- 8 依赖注入 点击这里
- 9 应用启动后台任务 点击这里
- 10 中间件构建 点击这里
- 11 线程spinwait 点击这里
以上就是笔者在源码阅读阶段,其总结的自我感觉重要的知识点在微软文档中的对应位置。
源码解析
这部分笔者根据上图中的四大块分别进行源码解析,可能篇幅比较长,其主要是对源代码增加了自己理解的注释,所以读者在阅读的过程中,要多注意源码中的注释(ps:展示出的代码不是全部代码,而只是重要代码哦,每个小节总结的点都是按照代码顺序解释)
初始化默认配置ConfigDefaultBuilder
public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[] args)
{
//设置程序运行路径
builder.UseContentRoot(Directory.GetCurrentDirectory());
builder.ConfigureHostConfiguration(config =>
{
//添加获取环境变量的前缀
config.AddEnvironmentVariables(prefix: "DOTNET_");
//添加命令行参数
if (args is { Length: > 0 })
{
config.AddCommandLine(args);
}
});
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
//宿主机环境信息
IHostEnvironment env = hostingContext.HostingEnvironment;
//是否在文件改变时重新加载,默认是True
bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
//默认添加的配置文件(格外添加以环境变量为名称的文件)
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
//如果是开发环境,并且应用程序的应用名称不是空字符串,则加载用户机密,默认true(主要是为了不同开发人员的配置不同)
if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly is not null)
{
config.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
}
}
//这里再次执行是为了让环境变量和命令行参数的配置优先级提高(后加载的key/value获取时优先级最高)
//添加其他环境变量
config.AddEnvironmentVariables();
//添加命令行参数
if (args is { Length: > 0 })
{
config.AddCommandLine(args);
}
})
.ConfigureLogging((hostingContext, logging) =>
{
//判断操作系统
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
//添加过滤规则,捕获warning日志
logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
}
//获取Logging配置
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
//添加输出到控制台
logging.AddConsole();
//添加将debug日志输出到控制台
logging.AddDebug();
//添加写入的事件源
logging.AddEventSourceLogger();
if (isWindows)
{
//添加事件日志
logging.AddEventLog();
}
//添加链路追踪选项
logging.Configure(options =>
{
options.ActivityTrackingOptions =
ActivityTrackingOptions.SpanId |
ActivityTrackingOptions.TraceId |
ActivityTrackingOptions.ParentId;
});
})
.UseDefaultServiceProvider((context, options) =>
{
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
//依赖注入相关校验
options.ValidateScopes = isDevelopment;
options.ValidateOnBuild = isDevelopment;
});
return builder;
}
源码总结:
- 设置程序执行路径以及获取环境变量和加载命令行参数。
- 根据环境变量加载appsettings.json,加载用户机密数据(仅开发环境)。
- 接着又加载环境变量和命令行参数(这里为什么又加载了一次呢?是因为这它们执行的顺序是不一样的,而后加载的会覆盖前面加载的Key/Value,前面加载主要是确定当前运行的环境变量以及用户自定义的命令行参数,后面是为确保通过key获取value的时候能够获取到准确的值)。
- 接下来就主要是配置默认Log,如果是开发环境,依赖注入相关的配置默认开启(验证scope是否被用于singleton,验证是否在调用期间就创建所有服务至缓存)。
初始化主机启动配置ConfigureWebHostDefaults
public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
{
_builder = builder;
var configBuilder = new ConfigurationBuilder()
.AddInMemoryCollection();
//添加以ASPNETCORE_开头的环境变量(ps:判断当前环境是那个环境)
if (!options.SuppressEnvironmentConfiguration)
{
configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_");
}
//这里主要加载环境变量
_config = configBuilder.Build();
_builder.ConfigureHostConfiguration(config =>
{
//将上面的配置加载进来
config.AddConfiguration(_config);
//通过配置和特性加载额外的Config(或者不加载配置),通过继承IHostingStartup无侵入性加载。
ExecuteHostingStartups();
});
//将上面Startup中Config的配置放到Build阶段加载
_builder.ConfigureAppConfiguration((context, configurationBuilder) =>
{
if (_hostingStartupWebHostBuilder != null)
{
var webhostContext = GetWebHostBuilderContext(context);
_hostingStartupWebHostBuilder.ConfigureAppConfiguration(webhostContext, configurationBuilder);
}
});
//增加注入的服务
_builder.ConfigureServices((context, services) =>
{
var webhostContext = GetWebHostBuilderContext(context);
var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
//注入一些其他服务
services.AddSingleton(webhostContext.HostingEnvironment);
services.AddSingleton((AspNetCore.Hosting.IHostingEnvironment)webhostContext.HostingEnvironment);
services.AddSingleton<IApplicationLifetime, GenericWebHostApplicationLifetime>();
services.Configure<GenericWebHostServiceOptions>(options =>
{
options.WebHostOptions = webHostOptions;
options.HostingStartupExceptions = _hostingStartupErrors;
});
services.TryAddSingleton(sp => new DiagnosticListener("Microsoft.AspNetCore"));
services.TryAddSingleton<DiagnosticSource>(sp => sp.GetRequiredService<DiagnosticListener>());
services.TryAddSingleton(sp => new ActivitySource("Microsoft.AspNetCore"));
services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();
_hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);
//可以通过配置的方式查找程序集加载StartUp,但是默认只会加载最后一个StartUp
if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
{
try
{
var startupType = StartupLoader.FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName);
UseStartup(startupType, context, services);
}
catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
{
var capture = ExceptionDispatchInfo.Capture(ex);
services.Configure<GenericWebHostServiceOptions>(options =>
{
options.ConfigureApplication = app =>
{
capture.Throw();
};
});
}
}
});
}
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
//提供.netCore 静态Web资产(ps:说实话这里不知道有什么用)
builder.ConfigureAppConfiguration((ctx, cb) =>
{
if (ctx.HostingEnvironment.IsDevelopment())
{
StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
}
});
//使用 Kestrel 配置反向代理
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
})
.ConfigureServices((hostingContext, services) =>
{
//配置启动的Url
services.PostConfigure<HostFilteringOptions>(options =>
{
if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
{
var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
}
});
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
//用来获取客户端的IP地址
if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
{
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
}
//添加路由配置
services.AddRouting();
})
//默认使用IIS
.UseIIS()
//使用进程内的托管模式
.UseIISIntegration();
}
这部分内容可能会多点,源码总结:
- 添加Memory缓存,添加ASPNETCORE_开头的环境变量。
- 根据用户的配置,来加载额外的StartUp中Config配置,但是它的参数是IWebHostBuilder,这部分可以参考微软文档StartUp的部分。
- 如果有存在这些配置的话,则统一放到Build阶段加载。
- 加载web主机需要的注入的服务,以及判断是否需要通过程序集来加载StartUp,并且添加一个程序启动时调用的服务(这里主要是构建HttpContext执行管道)。
- 引用Kestrel,继承路由和IIS,并且默认使用进程内托管。
- 加载用户自定义的其他配置,例如默认的调用UseStartup方法。
根据指定配置开始初始化主机Build
public class HostBuilder : IHostBuilder
{
private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<HostBuilderContext, IServiceCollection>>();
private List<IConfigureContainerAdapter> _configureContainerActions = new List<IConfigureContainerAdapter>();
private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());
private bool _hostBuilt;
private IConfiguration _hostConfiguration;
private IConfiguration _appConfiguration;
private HostBuilderContext _hostBuilderContext;
private HostingEnvironment _hostingEnvironment;
private IServiceProvider _appServices;
private PhysicalFileProvider _defaultProvider;
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
_configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
{
_serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory)));
return this;
}
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory)
{
_serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(() => _hostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory)));
return this;
}
public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
{
_configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate
?? throw new ArgumentNullException(nameof(configureDelegate))));
return this;
}
public IHost Build()
{
//只能执行一次这个方法
if (_hostBuilt)
{
throw new InvalidOperationException(SR.BuildCalled);
}
_hostBuilt = true;
using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
const string hostBuildingEventName = "HostBuilding";
const string hostBuiltEventName = "HostBuilt";
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
{
Write(diagnosticListener, hostBuildingEventName, this);
}
//执行Host配置(应用程序执行路径,加载_dotnet环境变量,获取命令行参数,加载预配置)
BuildHostConfiguration();
//设置主机环境变量
CreateHostingEnvironment();
//构建HostBuilderContext实例
CreateHostBuilderContext();
//构建程序配置(加载appsetting.json,环境变量,命令行参数等)
BuildAppConfiguration();
//构造容器,注入服务
CreateServiceProvider();
var host = _appServices.GetRequiredService<IHost>();
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
{
Write(diagnosticListener, hostBuiltEventName, host);
}
return host;
}
private static void Write<T>(
DiagnosticSource diagnosticSource,
string name,
T value)
{
diagnosticSource.Write(name, value);
}
private void BuildHostConfiguration()
{
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.AddInMemoryCollection();
foreach (Action<IConfigurationBuilder> buildAction in _configureHostConfigActions)
{
buildAction(configBuilder);
}
//本质是执行ConfigureProvider中的Load方法,加载对应配置
_hostConfiguration = configBuilder.Build();
}
private void CreateHostingEnvironment()
{
//设置环境变量
_hostingEnvironment = new HostingEnvironment()
{
ApplicationName = _hostConfiguration[HostDefaults.ApplicationKey],
EnvironmentName = _hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production,
ContentRootPath = ResolveContentRootPath(_hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory),
};
if (string.IsNullOrEmpty(_hostingEnvironment.ApplicationName))
{
_hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name;
}
//程序运行路径
_hostingEnvironment.ContentRootFileProvider = _defaultProvider = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath);
}
private void CreateHostBuilderContext()
{
_hostBuilderContext = new HostBuilderContext(Properties)
{
HostingEnvironment = _hostingEnvironment,
Configuration = _hostConfiguration
};
}
private void BuildAppConfiguration()
{
//对于已经加载过的配置不再重新加载
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.SetBasePath(_hostingEnvironment.ContentRootPath)
.AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);
//注意这里是AppConfig
foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
{
buildAction(_hostBuilderContext, configBuilder);
}
_appConfiguration = configBuilder.Build();
//将新的配置赋值给config
_hostBuilderContext.Configuration = _appConfiguration;
}
private void CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
services.AddSingleton(_hostBuilderContext);
services.AddSingleton(_ => _appConfiguration);
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
services.AddSingleton<IHost>(_ =>
{
return new Internal.Host(_appServices,
_hostingEnvironment,
_defaultProvider,
_appServices.GetRequiredService<IHostApplicationLifetime>(),
_appServices.GetRequiredService<ILogger<Internal.Host>>(),
_appServices.GetRequiredService<IHostLifetime>(),
_appServices.GetRequiredService<IOptions<HostOptions>>());
});
services.AddOptions().Configure<HostOptions>(options => { options.Initialize(_hostConfiguration); });
services.AddLogging();
//主要加载额外注入的服务
foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
{
configureServicesAction(_hostBuilderContext, services);
}
//这里返回object,主要是为了保留扩展,让用户自定义的依赖注入框架能够运行。
object containerBuilder = _serviceProviderFactory.CreateBuilder(services);
foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
{
containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
}
_appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);
if (_appServices == null)
{
throw new InvalidOperationException(SR.NullIServiceProvider);
}
//可能是想先把IConfiguration加载到内存中
_ = _appServices.GetService<IConfiguration>();
}
}
在上面的两个小单元可以看出,所有的构造是以委托的方式,最后都加载到HostBuilder内部的委托集合中,源码总结:
- 加载环境变量和命令行参数。
- 构建HostingEnvironment对象。
- 构建HostBuilderContext对象,里面包含配置和执行环境。
- 加载appsettings.json,环境变量和命令行参数等。
- 注入一些必须的服务,加载日志配置,WebHost里面注入的服务,加载StartUp里面ConfigService里面的服务,以及其他的一些注入的服务,构建容器(最后那里获取IConfiguration猜测可能是想先缓存到根容器吧)。
运行主机Run
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger.Starting();
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
CancellationToken combinedCancellationToken = combinedCancellationTokenSource.Token;
//应用程序启动和关闭事件
await _hostLifetime.WaitForStartAsync(combinedCancellationToken).ConfigureAwait(false);
combinedCancellationToken.ThrowIfCancellationRequested();
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
//主要是执行一些后台任务,可以重写启动和关闭时要做的操作
foreach (IHostedService hostedService in _hostedServices)
{
//立即执行的任务,例如构建管道就是在这里
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
//执行一些后台任务
if (hostedService is BackgroundService backgroundService)
{
_ = TryExecuteBackgroundServiceAsync(backgroundService);
}
}
//通知应用程序启动成功
_applicationLifetime.NotifyStarted();
//程序启动
_logger.Started();
}
源码总结:
- 监听程序的启动关闭事件。
- 开始执行Hosted服务或者加载后台执行的任务。
- 通过TaskCompletionSource来持续监听Token,hold住进程。
总结
通过模板来构建的.Net泛型主机,其实已经可以满足大部分的要求,并且微软保留大量扩展让用户来自定义,当然你也可以构建其他不同的主机类型(如:Web主机或者控制台程序启动项配置),想了解的可以点击这里。
以上就是笔者通过阅读源码来分析的程序执行流程,因为篇幅问题没有把所有代码都放出来,实在是太多了,所以只放了部分代码,主要是想给阅读源码的同学在阅读的时候找到思路,可能会有一点错误,还请评论指正😁。
关注公众号,不定期分享原创干货知识 |
---|