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

相關文章
相關標籤/搜索