Net6Configuration & Options 原始碼分析 Part3 IOptionsMonitor 是如何接收到配置檔案變更並同步資料來源的
配置源的同步 IOptionsMonitor 使用
//以下demo演示使用IOptionsMonitor重新載入配置並當重新載入配置是執行回撥函式
var configuration = new ConfigurationBuilder().AddJsonFile(path: "profile.json", optional: false, reloadOnChange: true).Build(); new ServiceCollection().AddOptions().Configure<Profile>(configuration).BuildServiceProvider().GetRequiredService<IOptionsMonitor<Profile>>().OnChange(profile => Console.WriteLine($"data reload: {profile.Age}")); Console.Read(); public class Profile { public int Age { get; set; } }
配置源的同步 IOptionsMonitor 原始碼分析
當檔案變更時如何向外傳送通知的以及 Reload data。
以JsonConfiguration為例:
FileConfigurationProvider通過FileProvider.Watch當檔案發生改變的時候會呼叫Load,load方法做了兩件事情,1.呼叫子類同名虛方完成具體資料的reload data(由具體實現類:JsonConfigurationProvider)2。提供呼叫OnReload(由父類ConfigurationProvider實現)。完成對外發送data change的通知。OnReload內呼叫了_reloadToken.OnReload傳送回撥通知併產生一個新的ConfigurationReloadToken重新賦值給_reloadToken,通知註冊到FileConfigurationProvider._reloadToken的回撥,那麼想接收到檔案改變的訊息只需要通過GetReloadToken()得到_reloadToken屬性並將回撥函式註冊進去即可。
如下是此三個類的繼承關係JsonConfiguration->FileConfigurationProvider->ConfigurationProvider
知道了這些在看下ConfigurationRoot。
public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable { public FileConfigurationProvider(FileConfigurationSource source!!) { Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { _changeTokenRegistration = ChangeToken.OnChange( () => Source.FileProvider.Watch(Source.Path!), () => { // 重新從JsonFile Load 資料並 Load(reload: true); }); } } private void Load(bool reload) { IFileInfo? file = Source.FileProvider?.GetFileInfo(Source.Path ?? string.Empty); using Stream stream = OpenRead(file); try { // 此處呼叫具體實現類的Load 方法例如JsonConfigurationProvider Load(stream); } // 傳送OnReload 並重新生成ConfigurationReloadToken共下次使用。 OnReload(); } } public class JsonConfigurationProvider : FileConfigurationProvider { public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { } public override void Load(Stream stream) { Data = JsonConfigurationFileParser.Parse(stream); } } public abstract class ConfigurationProvider : IConfigurationProvider { protected void OnReload() { ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); previousToken.OnReload(); } public IChangeToken GetReloadToken() { return _reloadToken; } }
ConfigurationRoot會迴圈呼叫把所有的providers
並通過IConfigurationProvider.GetReloadToken()得到FileConfigurationProvider._reloadToken,然後註冊上RaiseChanged作為回撥函式。以檔案系統為例,當檔案發生改動時會呼叫此回撥函式,此回撥函式又會呼叫ConfigurationRoot的_changeToken.OnReload()向外傳送通知。
ConfigurationChangeTokenSource:註冊的時機為ConfigurationChangeTokenSource.Configure.
我們作為使用者註冊的回撥事件就是註冊在OptionsMonitor._onChange中。當用戶使用OptionsMonitor時,其在構造方法通過DI拿到使用ConfigurationChangeTokenSource作為包裝類,其包裝的是ConfigurationRoot._changeToken,並把自身的事件OptionsMonitor._onChange作為回撥函式註冊在包裝類ConfigurationChangeTokenSource.包裝的ConfigurationRoot._changeToken中。自此完成了整個回撥鏈條。
// ConfigurationRoot向IConfigurationProvider註冊回撥函式拼接回調鏈條。
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
p.Load();
// 回撥鏈條拼接
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
private void RaiseChanged()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
}
// ConfigurationChangeTokenSource 包裝類與註冊 OptionsConfigurationServiceCollectionExtensions
public class ConfigurationChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>
{
private IConfiguration _config;
public ConfigurationChangeTokenSource(IConfiguration config) : this(Options.DefaultName, config){}
public IChangeToken GetChangeToken()
{
return _config.GetReloadToken();
}
}
public static class OptionsConfigurationServiceCollectionExtensions
{
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services!!, string? name, IConfiguration config!!, Action<BinderOptions>? configureBinder) where TOptions : class
{
services.AddOptions();
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}
}
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptionsMonitor<TOptions>, IDisposable
where TOptions : class
{
internal event Action<TOptions, string>? _onChange;
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
private void InvokeChanged(string? name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
TOptions options = Get(name);
if (_onChange != null)
{
_onChange.Invoke(options, name);
}
}
}
public IDisposable OnChange(Action<TOptions, string> listener)
{
var disposable = new ChangeTrackerDisposable(this, listener);
_onChange += disposable.OnChange;
return disposable;
}
}
總結
整個過程回撥使用了兩個ConfigurationReloadToken分別是。1. FileConfigurationProvider提供了一個ConfigurationReloadToken 2.提供了一個ConfigurationRoot._changeToken 。回撥鏈條的拼接是。1.FileConfigurationProvider建構函式中檔案的Watch與FileConfigurationProvider._reloadToken同時在這裡也完成了資料的reload data 2 ConfigurationRoot的建構函式中與IConfigurationProvider._reloadToken進行的回撥鏈條拼接 。第三次拼接是把使用者註冊的回撥函註冊在OptionsMonito的event上,OptionsMonito在建構函式中通過DI容器獲取到ConfigurationRoot._changeToken中包裝類。並把event作為回撥函式進行註冊.
通過以上程式碼分析,當我們向建立一個具有相同通知機制的回撥鏈條並且有多次通知 需要利用CancellationToken與 ChangeToken.OnChange 進行連結,同時要注意每次連結後向下發送訊息時,要重新生成changeToken,因為changeToken的特性是隻能傳送一次訊息。向多次必須重新生成ChangeToken例如
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();