asp.netcore 深入瞭解配置檔案載入過程
前言
配置檔案中程式執行中,擔當著不可或缺的角色;通常情況下,使用 visual studio 進行建立專案過程中,專案配置檔案會自動生成在專案根目錄下,如 appsettings.json,或者是被大家廣泛使用的 appsettings.{env.EnvironmentName}.json;配置檔案
作為一個入口,可以讓我們在不更新程式碼的情況,對程式進行干預和調整,那麼對其載入過程的全面瞭解就顯得非常必要。
何時載入了預設的配置檔案
在 Program.cs 檔案中,檢視以下程式碼
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 物件將業務系統的自定義物件加入服務上下文中,以方便後續介面注入使用。
AddJsonFile 方法的使用
通常情況下,我們都會使用預設的配置檔案進行開發,或者使用 appsettings.{env.EnvironmentName}.json 的檔名稱方式來區分 開發/測試/產品 環境,根據環境變數載入不同的配置檔案;可是這樣一來帶來了另外一個管理上的問題,產品環境的配置引數和開發環境
是不同的,如果使用環境變數的方式控制配置檔案的載入,則可能導致密碼洩露等風險;誠然,可以手工在產品環境建立此檔案,但是這樣一來,釋出流程將會變得非常繁瑣,稍有錯漏檔案便會被覆蓋。
我們推薦使用 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 中配置檔案載入的內部執行過程,從中我們認識到,預設配置檔案是如何載入,並將預設配置檔案如何注入到系統中的,還學習到了如果在不同的環境下,選擇載入自定義配置檔案的過程;但配置檔案變動的時候,系統內部又是如何去把配置檔案重新載入到記憶體中去的。