Asp.NetCore源碼學習[1-1]:配置[Configuration]
在Asp. NetCore中,配置系統支持不一樣的配置源(文件、環境變量等),雖然有多種的配置源,可是最終提供給系統使用的只有一個對象,那就是
ConfigurationRoot
。其內部維護了一個集合,用於保存各類配置源的IConfigurationProvider
。IConfigurationProvider
提供了對配置源的實際訪問。當經過key去ConfigurationRoot
查找對應的Value時,實際上會經過遍歷IConfigurationProvider
去查找對應的鍵值。 本篇文章主要描述ConfigurationRoot
對象的構建過程。git
Asp.NetCore
入口點代碼CreateWebHostBuilder(args).Build().Run();
Asp.NetCore
部分源碼WebHostBuilder
內部維護了_configureAppConfigurationBuilder
字段,其類型是 Action<WebHostBuilderContext, IConfigurationBuilder>
,該委託用於對ConfigurationBuilder
進行配置。首先在構造函數中先將環境變量的配置加載到 _config
字段中,用於設置默認監聽目錄爲程序執行目錄。CreateDefaultBuilder
方法中經過調用ConfigureAppConfiguration
方法保存委託,而後在Build
方法中構建配置系統目標類ConfigurationRoot
,最後經過單例模式注入到依賴系統中。github
public class WebHostBuilder { private Action<WebHostBuilderContext, IConfigurationBuilder> _configureAppConfigurationBuilder; private IConfiguration _config; public WebHostBuilder() { _hostingEnvironment = new HostingEnvironment(); /// _config = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "ASPNETCORE_") .Build(); } public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate) { _configureAppConfigurationBuilder += configureDelegate; return this; } public IWebHost Build() { var builder = new ConfigurationBuilder(); //經過委託配置IConfigurationBuilder _configureAppConfigurationBuilder?.Invoke(_context, builder); //構建ConfigurationRoot var configuration = builder.Build(); // register configuration as factory to make it dispose with the service provider services.AddSingleton<IConfiguration>(_ => configuration); } } public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = new WebHostBuilder(); builder.ConfigureAppConfiguration((hostingContext, config) => { //爲 IConfigurationBuilder 註冊配置源(JsonConfigurationSource) config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); }); return builder; }
Asp.NetCore
代碼,寫靜態測試方法public class ConfigurationTest { public static void Run() { //1.實例化ConfigurationBuilder var builder = new ConfigurationBuilder(); //2.增長配置源 builder.AddJsonFile(null, "appsettings.json", true,true); //3.構建ConfigurationRoot對象 var configuration = builder.Build(); //觀察ConfigurationRoot是否發生更改 Task.Run(() => { ChangeToken.OnChange(() => configuration.GetReloadToken(), () => { Console.WriteLine("Configuration has changed"); }); }); Thread.Sleep(60000); } }
ConfigurationBuilder
類構建目標類ConfigurationRoot
ConfigurationBuilder
是配置系統的構建類,經過Build
方法構建配置系統的目標類ConfigurationRoot
。其維護了一個用於保存IConfigurationSource
的集合,IConfigurationSource
用於提供IConfigurationProvider
。在Build
方法中,遍歷IList
構建IConfigurationProvider
對象,而後將IConfigurationProvider
集合傳到ConfigurationRoot
的構造函數中。代碼以下:json
/// <summary> /// 配置系統構建類 /// </summary> public class ConfigurationBuilder : IConfigurationBuilder { /// 配置源集合 public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); /// 增長一個新的配置源 public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } /// 經過配置源中提供的IConfigurationProvider構建配置根對象ConfigurationRoot 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); } }
IConfigurationSource
對象不單單用於建立IConfigurationProvider
,還保存了構建IConfigurationProvider
須要的依賴和配置選項。併發
ConfigurationRoot
類實現該類經過IList
進行初始化。其內部維護了類型爲ConfigurationReloadToken
的字段,該字段提供給外部,來進行全部配置源的監聽。每一個IConfigurationProvider
對象一樣維護了類型爲ConfigurationReloadToken
的字段。當IConfigurationProvider
監測到配置源發生更改時,更改IConfigurationProvider.IChangeToken
的狀態
在構造函數中執行如下操做:app
IConfigurationProvider.Load()
從配置源(文件、環境變量等)加載配置項ChangeToken.OnChange()
方法 監聽每一個IConfigurationProvider.IChangeToken
的狀態改變,當其狀態發生改變時更改ConfigurationRoot.IChangeToken
的狀態。(在ConfigurationRoot
外部能夠經過監聽IChangeToken狀態的改變,得知配置源發生了改變)/// <summary> /// 配置系統的根節點 /// </summary> public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList<IConfigurationProvider> _providers; private readonly IList<IDisposable> _changeTokenRegistrations; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); /// <summary> /// 使用IConfigurationProvider集合初始化ConfigurationRoot /// </summary> /// <param name="providers">The <see cref="IConfigurationProvider"/>s for this configuration.</param> public ConfigurationRoot(IList<IConfigurationProvider> providers) { _providers = providers ?? throw new ArgumentNullException(nameof(providers)); _changeTokenRegistrations = new List<IDisposable>(providers.Count); foreach (var p in providers) { p.Load(); //將每一個IConfigurationProvider的change token與ConfigurationRoot 的change token綁定 //當IConfigurationProvider._cts.Cancel()觸發時,觸發當ConfigurationRoot._cts.Cancel() _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); } } public IEnumerable<IConfigurationProvider> Providers => _providers; /// 遍歷_providers來設置、獲取配置項的鍵值對 public string this[string key] { get { for (var i = _providers.Count - 1; i >= 0; i--) { var provider = _providers[i]; if (provider.TryGet(key, out var value)) { return value; } } return null; } set { if (!_providers.Any()) { throw new InvalidOperationException("Can't find any IConfigurationProvider"); } foreach (var provider in _providers) { provider.Set(key, value); } } } /// 獲取IChangeToken,用於供外部使用者收到配置改變的消息通知 public IChangeToken GetReloadToken() => _changeToken; public void Reload() { foreach (var provider in _providers) { provider.Load(); } RaiseChanged(); } /// 生成一個新的change token,並觸發ConfigurationRoot的change token(舊)狀態改變 private void RaiseChanged() { var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()); previousToken.OnReload(); } /// <inheritdoc /> public void Dispose() { // dispose change token registrations foreach (var registration in _changeTokenRegistrations) { registration.Dispose(); } // dispose providers foreach (var provider in _providers) { (provider as IDisposable)?.Dispose(); } } }
ConfigurationReloadToken
的實現其使用適配器模式,經過CancellationTokenSource
實現IChangeToken
接口。代碼以下:asp.net
/// <summary> /// 用於發送更改通知 /// </summary> public interface IChangeToken { /// 指示是否發生更改 bool HasChanged { get; } /// 指示token是否會主動調用callbacks,false的狀況下:token的消費者須要輪詢 HasChanged 屬性檢測是否發生更改 bool ActiveChangeCallbacks { get; } /// 註冊回調函數, 更改發生時(HasChanged爲true),會被調用(只會被調用一次) IDisposable RegisterChangeCallback(Action<object> callback, object state); }
/// 基於CancellationTokenSource實現IChangeToken接口(適配器模式) public class ConfigurationReloadToken:IChangeToken { private CancellationTokenSource _cts = new CancellationTokenSource(); /// CancellationTokenSource會主動調用callbacks,因此爲true public bool ActiveChangeCallbacks => true; public bool HasChanged => _cts.IsCancellationRequested; public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _cts.Token.Register(callback, state); public void OnReload() => _cts.Cancel(); }
CancellationTokenSource
對象基於協做取消模式設計的對象,用於取消異步操做或者長時間同步操做。( .NET指南/取消託管線程)異步
CancellationTokenSource
對象的特色:ide
ChangeToken.OnChange
靜態方法實現更改通知的持續消費因爲CancellationTokenSource.Cancel只會觸發一次callbacks,須要ChangeToken.OnChange來實現持續監聽取消通知。
實現原理:每次須要發生更改通知時,首先生成一個新的cts,而後改變舊的cts狀態,觸發回調函數,最後將新的cts與回調函數綁定。函數
/// <summary> /// 將changeToken消費者註冊到IChangeToken的回調函數中,並實現IChangeToken狀態改變的持續消費 /// </summary> public static class ChangeToken { /// 爲changetoken生產者綁定消費者. /// 1.在IChangeToken的狀態未改變的狀況下,生產者每次返回相同的IChangeToken /// 2.狀態改變時,生產者生成新的IChangeToken,消費者執行響應動做,爲新的IChangeToken綁定消費者,釋放舊的IChangeToken public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer) { if (changeTokenProducer == null) { throw new ArgumentNullException(nameof(changeTokenProducer)); } if (changeTokenConsumer == null) { throw new ArgumentNullException(nameof(changeTokenConsumer)); } return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer); } private class ChangeTokenRegistration<TState> : IDisposable { private readonly Func<IChangeToken> _changeTokenProducer; private readonly Action<TState> _changeTokenConsumer; private readonly TState _state; private IDisposable _disposable;//用於保存當前正在使用的 IChangeToken private static readonly NoopDisposable _disposedSentinel = new NoopDisposable(); public ChangeTokenRegistration(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state) { _changeTokenProducer = changeTokenProducer; _changeTokenConsumer = changeTokenConsumer; _state = state; var token = changeTokenProducer(); RegisterChangeTokenCallback(token); } /// 1.先執行消費者動做,再綁定新的token,防止消費者執行並發動做 /// 2.不然的話可能出現如下狀況:若是在綁定新token的回調方法後,而且在執行callback以前,新token的狀態發生了改變,此時第二次callback也會執行,這樣會形成callback的併發執行。 private void OnChangeTokenFired() { var token = _changeTokenProducer(); try { _changeTokenConsumer(_state); } finally { RegisterChangeTokenCallback(token); } } private void RegisterChangeTokenCallback(IChangeToken token) { var registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>)s).OnChangeTokenFired(), this); SetDisposable(registraton); } /// 1.將當前使用的IChangeToken保存到_disposable字段中 /// 2.若是本對象已經釋放,馬上釋放新產生的 IChangeToken /// 3.已經失效的 IChangeToken 因爲已經不被引用,等待GC自動釋放(爲何不手動釋放?) private void SetDisposable(IDisposable disposable) { // 讀取當前保存的 IChangeToken var current = Volatile.Read(ref _disposable); // 若是本對象已經釋放,馬上釋放新產生的IChangeToken if (current == _disposedSentinel) { disposable.Dispose(); return; } // 不然更新_disposable字段,返回原值 var previous = Interlocked.CompareExchange(ref _disposable, disposable, current); // current = 以前的 IChangeToken if (previous == _disposedSentinel) { // 更新失敗 說明對象已釋放 previous = _disposedSentinel // 本對象已經釋放,馬上釋放新產生的IChangeToken disposable.Dispose(); } else if (previous == current) { // 更新成功 previous 是以前的 IChangeToken } else { // 若是其餘人爲 _disposable賦值,且值不爲 _disposedSentinel // 會形成對象未釋放、更新失敗的狀況 throw new InvalidOperationException("Somebody else set the _disposable field"); } } // 釋放當前保存的 IChangeToken,將字段賦值爲_disposedSentinel public void Dispose() { Interlocked.Exchange(ref _disposable, _disposedSentinel).Dispose(); } private class NoopDisposable : IDisposable { public void Dispose() { } } } }
ChangeToken.OnChange
測試方法該測試方法經過一個ChangeTokenProducer
類來模擬內部的狀態改變。經過內部維護一個ConfigurationReloadToken
,能夠向外部發出更改通知(一次性)。爲了實現向外部持續發出更改通知,能夠在更改ConfigurationReloadToken
狀態以前,從新實例化有一個新的IChangeToken
,供外部從新綁定回調方法。oop
class ChangeTokenTest { public static void Run() { var ctsProducer = new ChangeTokenProducer(); var subscriber = ChangeToken.OnChange(() => ctsProducer.GetReloadToken(), () => { Console.WriteLine("消費者觀察到改變事件"); }); Console.ReadLine(); } /// <summary> /// 假設該類須要在內部狀態發生改變時向外界發送更改通知 /// </summary> private class ChangeTokenProducer { // cts只能執行一次相應動做 private ConfigurationReloadToken _changetoken = new ConfigurationReloadToken(); /// <summary> /// 模擬狀態改變 /// </summary> public ChangeTokenProducer() { Task.Run(()=> { while (true) { Thread.Sleep(3000);//模擬耗時 //內部狀態發生改變,通知外部 RaiseChanged(); } }); } public IChangeToken GetReloadToken () => _changetoken; private void RaiseChanged() { //產生新的cts var previousToken = Interlocked.Exchange(ref _changetoken, new ConfigurationReloadToken()); //觸發老的cts動做 //外界執行響應動做時,經過GetReloadToken()獲取新的cts,執行相應動做,並從新綁定回調函數 previousToken.OnReload(); } } }
IConfigurationSource
的實現IConfigurationSource
擁有一個實現IFileProvider
接口的類屬性。默認實現爲PhysicalFileProvider
類,文件監控目錄默認爲程序集根目錄。該類提供文件的訪問和監控功能。在Build
方法中實例化JsonFileConfigurationProvider
,並將自身傳遞進去。
在.NetCore源碼中JsonConfigurationSource
是繼承 抽象類FileConfigurationSource
的。此處合併了兩個類的代碼。
public class JsonFileConfigurationSource : IConfigurationSource { public IFileProvider FileProvider { get; set; } public IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new JsonFileConfigurationProvider(this); } public void EnsureDefaults(IConfigurationBuilder builder) { FileProvider = FileProvider ?? builder.GetFileProvider(); } } public static class FileConfigurationExtensions { /// 獲取默認的IFileProvider,root目錄默認爲程序集根目錄(AppContext.BaseDirectory) public static IFileProvider GetFileProvider(this IConfigurationBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty); } }
IConfigurationProvider
的實現在Core的源碼中繼承關係爲JsonConfigurationProvider: FileConfigurationProvider:ConfigurationProvider:IConfigurationProvider
。本項目代碼合併了JsonConfigurationProvider FileConfigurationProvider
這兩個類
ConfigurationProvider
的實現該類使用一個字典用於保存配置項的字符串鍵值對。並擁有一個類型爲 ConfigurationReloadToken
的字段。在配置文件發生更改時,_reloadToken
的狀態發生改變,外部能夠經過觀察該字段的狀態來得知配置文件發生更改。
/// <summary> /// 配置提供者抽象類 /// </summary> public abstract class ConfigurationProvider : IConfigurationProvider { private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken(); /// 初始化存儲配置的字典,鍵值忽略大小寫 protected ConfigurationProvider() { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } /// 存儲配置的鍵值對,protected只能在子類中訪問 protected IDictionary<string, string> Data { get; set; } /// 讀取鍵值 public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); /// 設置鍵值 public virtual void Set(string key, string value) => Data[key] = value; /// 加載配置數據源,使用virtual修飾符,在子類中實現重寫 public virtual void Load() { } public IChangeToken GetReloadToken() { return _reloadToken; } /// <summary> /// 觸發change token,並生成一個新的change token /// </summary> protected void OnReload() { //原子操做:賦值並返回原始值 var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); previousToken.OnReload(); } }
JsonFileConfigurationProvider
的實現該類繼承於抽象類ConfigurationProvider
。
在構造函數中監聽 FileProvider.Watch()
方法返回的IChangeToken,收到更改通知時執行如下兩個動做,一個是從新讀取文件流,加載到字典中;另外一個是改變_reloadToken
的狀態,用於通知外部:已經從新加載配置文件。因爲在本項目中直接引用了MS的PhysicalFileProvider
,而該類監聽文件返回的是微軟的IChangeToken
。爲了兼容項目代碼,經過一個適配類來轉換接口。
public class JsonFileConfigurationProvider : ConfigurationProvider, IDisposable { private readonly IDisposable _changeTokenRegistration; public JsonFileConfigurationSource Source { get; } public JsonFileConfigurationProvider(JsonFileConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { //1.IFileProvider.Watch(string filter) 返回IChangeToken //2.綁定IChangeToken的回調函數(1。生成新的IChangeToken 2.讀取配置文件、向ConfigurationRoot傳遞消息) //3.檢測到文件更改時,觸發回調 //4.爲新的IChangeToken綁定回調函數 _changeTokenRegistration = ChangeToken.OnChange( () => new IChangeTokenAdapter(Source.FileProvider.Watch(Source.Path)), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } } //從新加載文件並向IConfigurationRoot傳遞更改通知 private void Load(bool reload) { var file = Source.FileProvider?.GetFileInfo(Source.Path); if (file == null || !file.Exists) { if (Source.Optional || reload) // Always optional on reload { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } else { //處理異常 } } else { // Always create new Data on reload to drop old keys if (reload) { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } using (var stream = file.CreateReadStream()) { try { Load(stream); } catch (Exception e) { HandleException(new FileLoadExceptionContext() { Exception = e, Provider = this, Ignore = true }); } } } //觸發IConfigurationProvider._cts.Cancel(),向IConfigurationRoot傳遞更改通知 OnReload(); } public override void Load() { Load(reload: false); } /// 從文件流中加載數據到IConfigurationProvider的Data中 public void Load(Stream stream) { try { //.NetCore3.0使用JsonDocument讀取json文件,生成結構化文檔: /// [key] 節點1-1:節點2-1:節點3-1 [value] Value1 /// [key] 節點1-1:節點2-2:節點3-2 [value] Value2 /// Data = JsonConfigurationFileParser.Parse(stream); //此處使用Newtonsoft.Json,簡單的序列化爲普通鍵值對 using (StreamReader sr = new StreamReader(stream)) { String jsonStr = sr.ReadToEnd(); Data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonStr); } } catch (Exception e) { throw new FormatException("讀取文件流失敗", e); } } public void Dispose() => Dispose(true); /// 釋放_changeTokenRegistration protected virtual void Dispose(bool disposing) { _changeTokenRegistration?.Dispose(); } } /// 適配器類 /// 將Microsoft.Extensions.Primitives.IChangeToken轉換爲CoreWebApp.Primitives.IChangeToken public class IChangeTokenAdapter : IChangeToken { public IChangeTokenAdapter(IChangeTokenMS msToken) { MsToken = msToken ?? throw new ArgumentNullException(nameof(msToken)); } private IChangeTokenMS MsToken { get; set; } public bool HasChanged => MsToken.HasChanged; public bool ActiveChangeCallbacks => MsToken.ActiveChangeCallbacks; public IDisposable RegisterChangeCallback(Action<object> callback, object state) { return MsToken.RegisterChangeCallback(callback, state); } }
ConfigurationSection
類的實現{ "OptionV1": { "OptionV21": "ValueV21", "OptionV22": { "OptionV31": "ValueV31", "OptionV32": "ValueV32" } } }
對於如上的配置文件會保存爲key "OptionV1:OptionV22:OptionV31" value "ValueV31"
的格式,這樣同時將節點間的層級關係也保存了下來。經過ConfigurationRoot
訪問鍵值須要提供鍵的全路徑。ConfigurationSection
類至關於定位了某個節點,經過ConfigurationSection
訪問鍵值只須要經過相對路徑。
到此爲止,ConfigurationRoot
已經構建完成,而後經過DI模塊以單例模式注入到系統中。在控制器中能夠經過IConfiguration
訪問到全部配置源的鍵值對,而且當配置文件發生改變時從新加載IConfigurationProvider
。下篇文章將會講述從如何經過強類型IOptions
訪問配置項。