asp.netcore 深刻了解配置文件加载过程

前言

    配置文件中程序运行中,担当着不可或缺的角色;一般状况下,使用 visual studio 进行建立项目过程当中,项目配置文件会自动生成在项目根目录下,如 appsettings.json,或者是被你们普遍使用的 appsettings.{env.EnvironmentName}.json;配置文件
做为一个入口,可让咱们在不更新代码的状况,对程序进行干预和调整,那么对其加载过程的全面了解就显得很是必要。json

什么时候加载了默认的配置文件

在 Program.cs 文件中,查看如下代码app

public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
  • WebHost.CreateDefaultBuilder 位于程序集 Microsoft.AspNetCore.dll 内,当程序执行 WebHost.CreateDefaultBuilder(args) 的时候,在 CreateDefaultBuilder 方法内部加载了默认的配置文件
    代码以下
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
        {
            var builder = new WebHostBuilder();

            if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))
            {
                builder.UseContentRoot(Directory.GetCurrentDirectory());
            }
            if (args != null)
            {
                builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
            }

            builder.UseKestrel((builderContext, options) =>
                {
                    options.Configure(builderContext.Configuration.GetSection("Kestrel"));
                })
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    if (env.IsDevelopment())
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                    logging.AddEventSourceLogger();
                })
                .ConfigureServices((hostingContext, services) =>
                {
                    // Fallback
                    services.PostConfigure<HostFilteringOptions>(options =>
                    {
                        if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
                        {
                            // "AllowedHosts": "localhost;127.0.0.1;[::1]"
                            var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                            // Fall back to "*" to disable.
                            options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
                        }
                    });
                    // Change notification
                    services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
                        new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));

                    services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
                })
                .UseIIS()
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) =>
                {
                    options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                });

            return builder;
        }
  • 能够看到,CreateDefaultBuilder 内部仍是使用了 IConfigurationBuilder 的实现,且写死了默认配置文件的名字
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
        {
            var builder = new WebHostBuilder();

            if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))
            {
                builder.UseContentRoot(Directory.GetCurrentDirectory());
            }
            if (args != null)
            {
                builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
            }

            builder.UseKestrel((builderContext, options) =>
                {
                    options.Configure(builderContext.Configuration.GetSection("Kestrel"));
                })
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    if (env.IsDevelopment())
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                    logging.AddEventSourceLogger();
                })
                .ConfigureServices((hostingContext, services) =>
                {
                    // Fallback
                    services.PostConfigure<HostFilteringOptions>(options =>
                    {
                        if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
                        {
                            // "AllowedHosts": "localhost;127.0.0.1;[::1]"
                            var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                            // Fall back to "*" to disable.
                            options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
                        }
                    });
                    // Change notification
                    services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
                        new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));

                    services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
                })
                .UseIIS()
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) =>
                {
                    options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                });

            return builder;
        }
  • 因为以上代码,咱们能够在应用程序根目录下使用 appsettings.json 和 appsettings.{env.EnvironmentName}.json 这种形式的默认配置文件名称
    而且,因为 Main 方法默认对配置文件进行了 Build 方法的调用操做
public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }
  • 咱们能够在 Startup.cs 中使用注入的方式得到默认的配置文件对象 IConfigurationRoot/IConfiguration,代码片断
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
  • 这是为何呢,由于在 执行 Build 方法的时候,方法内部已经将默认配置文件对象加入了 ServiceCollection 中,代码片断
var services = new ServiceCollection();
  services.AddSingleton(_options);
  services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
  services.AddSingleton<Extensions.Hosting.IHostingEnvironment>(_hostingEnvironment);
  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;

以上这段代码很是熟悉,由于在 Startup.cs 文件中,咱们也许会使用过 ServiceCollection 对象将业务系统的自定义对象加入服务上下文中,以方便后续接口注入使用。asp.net

AddJsonFile 方法的使用

    一般状况下,咱们都会使用默认的配置文件进行开发,或者使用 appsettings.{env.EnvironmentName}.json 的文件名称方式来区分 开发/测试/产品 环境,根据环境变量加载不一样的配置文件;但是这样一来带来了另一个管理上的问题,产品环境的配置参数和开发环境
是不一样的,若是使用环境变量的方式控制配置文件的加载,则可能致使密码泄露等风险;诚然,能够手工在产品环境建立此文件,可是这样一来,发布流程将会变得很是繁琐,稍有错漏文件便会被覆盖。ide

咱们推荐使用 AddJsonFile 加载产品环境配置,代码以下学习

public Startup(IConfiguration configuration, IHostingEnvironment env)
        {
            Configuration = AddCustomizedJsonFile(env).Build();

        }

        public ConfigurationBuilder AddCustomizedJsonFile(IHostingEnvironment env)
        {
            var build = new ConfigurationBuilder();
            build.SetBasePath(env.ContentRootPath).AddJsonFile("appsettings.json", true, true);
            if (env.IsProduction())
            {
                build.AddJsonFile(Path.Combine("/data/sites/config", "appsettings.json"), true, true);
            }
            return build;
        }
  •     经过 AddCustomizedJsonFile 方法去建立一个 ConfigurationBuilder 对象,并覆盖系统默认的 ConfigurationBuilder 对象,在方法内部,默认加载开发环境的配置文件,在产品模式下,额外加载目录 /data/sites/config/appsettings.json 文件,
    不一样担忧配置文件冲突问题,相同键值的内容将由后加入的配置文件所覆盖。

配置文件的变更

  • 在调用 AddJsonFile 时,咱们看到该方法共有 5 个重载的方法
    其中一个方法包含了 4 个参数,代码以下
public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException(Resources.Error_InvalidFilePath, nameof(path));
            }

            return builder.AddJsonFile(s =>
            {
                s.FileProvider = provider;
                s.Path = path;
                s.Optional = optional;
                s.ReloadOnChange = reloadOnChange;
                s.ResolveFileProvider();
            });
        }
  •     在此方法中,有一个参数 bool reloadOnChange,从参数描述可知,该值指示在文件变更的时候是否从新加载,默认值为:false;通常在手动加载配置文件,即调用 AddJsonFile 方法时,建议将该参数值设置为 true。
    那么 .netcore 是若是经过该参数 reloadOnChange 是来监控文件变更,以及什么时候进行从新加载的操做呢,看下面代码
public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (var source in Sources)
            {
                var provider = source.Build(this);
                providers.Add(provider);
            }
            return new ConfigurationRoot(providers);
        }
  • 在咱们执行 .Build 方法的时候,方法内部最后一行代码给咱们利用 AddJsonFile 方法的参数建立并返回了一个 ConfigurationRoot 对象
    在 ConfigurationRoot 的构造方法中
public ConfigurationRoot(IList<IConfigurationProvider> providers)
        {
            if (providers == null)
            {
                throw new ArgumentNullException(nameof(providers));
            }

            _providers = providers;
            foreach (var p in providers)
            {
                p.Load();
                ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged());
            }
        }
  • 咱们看到,方法内部一次读取了经过 AddJsonFile 方法加入的配置文件,并为每一个配置文件单独分配了一个监听器 ChangeToken,并绑定当前文件读取对象 IConfigurationProvider.GetReloadToken 方法到监听器中
    当文件产生变更的时候,监听器会收到一个通知,同时,对该文件执行原子操做
private void RaiseChanged()
        {
            var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
            previousToken.OnReload();
        }
  • 因为 AddJsonFile 方法内部使用了 JsonConfigurationSource ,而 Build 的重载方法构造了一个 JsonConfigurationProvider 读取对象,查看代码
public override IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            EnsureDefaults(builder);
            return new JsonConfigurationProvider(this);
        }
  • 在 JsonConfigurationProvider 继承自 FileConfigurationProvider 类,该类位于程序集 Microsoft.Extensions.Configuration.Json.dll 内
    在 FileConfigurationProvider 的构造方法中实现了监听器从新加载配置文件的过程
public FileConfigurationProvider(FileConfigurationSource source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
            Source = source;

            if (Source.ReloadOnChange && Source.FileProvider != null)
            {
                ChangeToken.OnChange(
                    () => Source.FileProvider.Watch(Source.Path),
                    () => {
                        Thread.Sleep(Source.ReloadDelay);
                        Load(reload: true);
                    });
            }
        }

值得注意的是,该监听器不是在获得文件变更通知后第一时间去从新加载配置文件,方法内部能够看到,这里有一个 Thread.Sleep(Source.ReloadDelay),而 ReloadDelay 的默认值为:250ms,该属性的描述为测试

  • 获取或者设置从新加载将等待的毫秒数, 而后调用 "Load" 方法。 这有助于避免在彻底写入文件以前触发从新加载。默认值为250
  • 让人欣慰的是,咱们能够自定义该值,若是业务对文件变更需求不是特别迫切,您能够将该值设置为一个很大的时间,一般状况下,咱们不建议那么作

结语

    以上就是 asp.netcore 中配置文件加载的内部执行过程,从中咱们认识到,默认配置文件是如何加载,并将默认配置文件如何注入到系统中的,还学习到了若是在不一样的环境下,选择加载自定义配置文件的过程;但配置文件变更的时候,系统内部又是如何去把配置文件从新加载到内存中去的。ui

相关文章
相关标签/搜索