1. 程式人生 > >Asp.NetCore原始碼學習[2-1]:配置[Configuration]

Asp.NetCore原始碼學習[2-1]:配置[Configuration]

Asp.NetCore原始碼學習[2-1]:配置[Configuration]

在Asp. NetCore中,配置系統支援不同的配置源(檔案、環境變數等),雖然有多種的配置源,但是最終提供給系統使用的只有一個物件,那就是ConfigurationRoot。其內部維護了一個集合,用於儲存各種配置源的IConfigurationProviderIConfigurationProvider提供了對配置源的實際訪問。當通過key去ConfigurationRoot查詢對應的Value時,實際上會通過遍歷IConfigurationProvider去查詢對應的鍵值。 本篇文章主要描述ConfigurationRoot

物件的構建過程。

本系列原始碼地址

Asp.NetCore 入口點程式碼

CreateWebHostBuilder(args).Build().Run();

Asp.NetCore 部分原始碼

WebHostBuilder內部維護了_configureAppConfigurationBuilder欄位,其型別是 Action<WebHostBuilderContext, IConfigurationBuilder>,該委託用於對ConfigurationBuilder進行配置。首先在CreateDefaultBuilder方法中通過呼叫ConfigureAppConfiguration

方法儲存委託,然後在Build方法中構建配置系統目標類ConfigurationRoot

public class WebHostBuilder
{
    private Action<WebHostBuilderContext, IConfigurationBuilder> _configureAppConfigurationBuilder;
    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) =>
    {
        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的建構函式中。程式碼如下:

    /// <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的狀態
在建構函式中執行以下操作:

  • 1 呼叫IConfigurationProvider.Load()從配置源(檔案、環境變數等)載入配置項
  • 2 通過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介面。程式碼如下:

    /// <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物件的特點:

  • 1 CancellationTokenSource.Token是值型別,傳遞副本
  • 2 呼叫 CancellationTokenSource.Cancel 方法提供取消通知後,CancellationTokenSource.Token的狀態發生改變,呼叫callbacks,並改變所有Token副本的狀態
  • 3 需要呼叫dispose釋放CancellationTokenSource
  • 4 多次呼叫CancellationTokenSource.Cancel,callbacks也只會執行一次
  • 5 再CancellationTokenSource.Cancel之後,新註冊的callback同樣也會被執行

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後,立刻失效,持續監聽失敗?
            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()
                {
                }
            }
        }
    }
結合測試場景會更加清楚
    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模組以單例模式注入到系統中。在控制器中可以通過IConfigurationRoot訪問到所有配置源的鍵值對。下篇文章將會講述從如何通過強型別IOptions 訪問配置項