Net6Configuration & Options 原始碼分析 Part3 IOptionsMonitor 是如何接收到配置檔案變更並同步資料來源的

一身大膘發表於2022-03-22

配置源的同步 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();

相關文章