.net 5.0 配置文件組件之JsonProvider源碼解析 C#下 觀察者模式的另一種實現方式IChangeToken和ChangeToken.OnChange

1、本文主要介紹下.net core 5.0的配置文件組件JsonProvider源碼核心邏輯.

直接上調用方式代碼,跟着代碼一步步解析

            var workDir = $"{Environment.CurrentDirectory}";
            var builder = new ConfigurationBuilder()
              .SetBasePath(workDir)
              .AddJsonFile($"test.json", optional: true, reloadOnChange: true);

            var root = builder.Build();

ok,首先看ConfigurationBuilder幹了什麼,源碼如下:

    public class ConfigurationBuilder : IConfigurationBuilder
    {
        public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

        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;
        }

        public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (IConfigurationSource source in Sources)
            {
                IConfigurationProvider provider = source.Build(this);
                providers.Add(provider);
            }
            return new ConfigurationRoot(providers);
        }
    }

ok,到這裏其實Builder沒幹啥,只是初始化了Properties和 Sources兩個實例,接着看SetBasePath擴展方法幹了什麼

        public static IConfigurationBuilder SetBasePath(this IConfigurationBuilder builder, string basePath)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (basePath == null)
            {
                throw new ArgumentNullException(nameof(basePath));
            }

            return builder.SetFileProvider(new PhysicalFileProvider(basePath));
        }

簡單的參數校驗,且調用了builder.SetFileProvider,代碼如下:

        public static IConfigurationBuilder SetFileProvider(this IConfigurationBuilder builder, IFileProvider fileProvider)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.Properties[FileProviderKey] = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
            return builder;
        }

到這裏很簡單,向Properties屬性集合寫入了PhysicalFileProvider了,並給PhysicalFileProvider傳入了根目錄.ok,接下去看PhysicalFileProvider的邏輯.

        public PhysicalFileProvider(string root, ExclusionFilters filters)
       {
            //路徑必須是絕對路徑
            if (!Path.IsPathRooted(root))
            {
                throw new ArgumentException("The path must be absolute.", nameof(root));
            }

            string fullRoot = Path.GetFullPath(root);
            Root = PathUtils.EnsureTrailingSlash(fullRoot);
            if (!Directory.Exists(Root))
            {
                throw new DirectoryNotFoundException(Root);
            }
            _filters = filters;
            _fileWatcherFactory = () => CreateFileWatcher();
        }

處理了下傳入的根目錄,且指定了過濾器ExclusionFilters,過濾器源碼如下:

   public enum ExclusionFilters
    {
        Sensitive = DotPrefixed | Hidden | System,

        DotPrefixed = 1,

        Hidden = 2,

        System = 4,

        None = 0
    }

這個特性只要是過濾掃描文件夾下的文件時,哪些文件是不能操作,關於這個邏輯,後續不再贅述了.接着看核心代碼CreateFileWatcher()

        internal PhysicalFilesWatcher CreateFileWatcher()
        {
            string root = PathUtils.EnsureTrailingSlash(Path.GetFullPath(Root));
            FileSystemWatcher watcher =  new FileSystemWatcher(root);
            return new PhysicalFilesWatcher(root, watcher, _filters);
        }

到這裏就很簡單了,很明顯組件用FileSystemWatcher監控了傳入的指定的根目錄.說明JsonProvider支持配置變更檢測.

至於爲什麼_fileWatcherFactory是個lamdba表達式,是因爲這裏做了懶加載操作,代碼如下:

        internal PhysicalFilesWatcher FileWatcher
        {
            get
            {
                return LazyInitializer.EnsureInitialized(
                    ref _fileWatcher,
                    ref _fileWatcherInitialized,
                    ref _fileWatcherLock,
                    _fileWatcherFactory);
            }
            set
            {
                Debug.Assert(!_fileWatcherInitialized);
                _fileWatcherInitialized = true;
                _fileWatcher = value;
            }
        }

當在PhysicalFileProvider中調用FileWatcher實例時會調用CreateFileWatcher()方法,這個在多線程中表現很好,不會重複初始化Watcher對象.

ok,到這裏先不介紹FileWatcher的通知機制,接着解析源碼AddJsonFile擴展方法.如下:

        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("path can not be null", nameof(path));
            }

            return builder.AddJsonFile(s =>
            {
                s.FileProvider = provider;
                s.Path = path;
                s.Optional = optional;
                s.ReloadOnChange = reloadOnChange;
                s.ResolveFileProvider();
            });
        }

參數校驗並調用builder.AddJsonFile方法源碼如下:

        public static IConfigurationBuilder Add<TSource>(this IConfigurationBuilder builder, Action<TSource> configureSource) where TSource : IConfigurationSource, new()
        {
            var source = new TSource();
            configureSource?.Invoke(source);
            return builder.Add(source);
        }

build.Add方法向ConfigurationBuilder實例添加了JsonConfigurationSource實例

        public IConfigurationBuilder Add(IConfigurationSource source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            Sources.Add(source);
            return this;
        }

ok,到這裏ConfigurationBuilder實例添加了PhysicalFileProvider實例和JsonConfigurationSource實例,接着看JsonConfigurationSource實例的內容

 return builder.AddJsonFile(s =>
            {
                s.FileProvider = provider;
                s.Path = path;
                s.Optional = optional;
                s.ReloadOnChange = reloadOnChange;
                s.ResolveFileProvider();
            });

ok,到這裏ConfigurationBuilder實例添加了PhysicalFileProvider實例和JsonConfigurationSource實例添加完成.說明ConfigurationBuilder實例相關屬性填充完畢,下面就要調用build方法了.build代碼如下:

    public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (IConfigurationSource source in Sources)
            {
                IConfigurationProvider provider = source.Build(this);
                providers.Add(provider);
            }
            return new ConfigurationRoot(providers);
        }

遍歷所有的IConfigurationSource,看下source.Build幹了什麼,代碼如下:

   public override IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            EnsureDefaults(builder);
            return new JsonConfigurationProvider(this);
        }

接着看EnsureDefaults方法:

        public void EnsureDefaults(IConfigurationBuilder builder)
        {
            FileProvider = FileProvider ?? builder.GetFileProvider();
            OnLoadException = OnLoadException ?? builder.GetFileLoadExceptionHandler();
        }

應爲按照示例代碼的調用方式,沒有顯示傳Provider所以,這裏從builder實例中獲取剛剛寫入的PhysicalFileProvider實例,並制定了文件加載異常的回調OnLoadException.

最後獲得一個完整的JsonConfigurationSource實例,並根據JsonConfigurationSource實例生成JsonConfigurationProvider實例.到這裏可以得出一個結論通過ConfigurationBuilder實例中的IConfigurationSource實例和IFileProvider實例,並通過調用ConfigurationBuilder實例的build方法可以得到JsonConfigurationProvider實例.下面看看JsonConfigurationProvider的代碼,如下:

    public class JsonConfigurationProvider : FileConfigurationProvider
    {
        public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }

        public override void Load(Stream stream)
        {
            try
            {
                Data = JsonConfigurationFileParser.Parse(stream);
            }
            catch (JsonException e)
            {
                throw new FormatException(e.Message);
            }
        }
    }

看base中的代碼:

        public FileConfigurationProvider(FileConfigurationSource source)
        {
            Source = source ?? throw new ArgumentNullException(nameof(source));

            if (Source.ReloadOnChange && Source.FileProvider != null)
            {
                _changeTokenRegistration = ChangeToken.OnChange(
                    () => Source.FileProvider.Watch(Source.Path),
                    () =>
                    {
                        Thread.Sleep(Source.ReloadDelay);
                        Load(reload: true);
                    });
            }
        }

ok,到這裏很清晰了,如果sonConfigurationSource實例中的ReloadOnChange參數設爲true,那麼就會開啓配置文件監聽(通過FileSystemWatcher類實現).接着看PhysicalFileProvider實例的Watch方法

        public IChangeToken Watch(string filter)
        {
            if (filter == null || PathUtils.HasInvalidFilterChars(filter))
            {
                return NullChangeToken.Singleton;
            }

            filter = filter.TrimStart(_pathSeparators);
            return FileWatcher.CreateFileChangeToken(filter);
        }

第一步,檢測傳入的文件名是否服務要求.接着看FileWatcher.CreateFileChangeToken

        public IChangeToken CreateFileChangeToken(string filter)
        {
            if (filter == null)
            {
                throw new ArgumentNullException(nameof(filter));
            }

            filter = NormalizePath(filter);

            if (Path.IsPathRooted(filter) || PathUtils.PathNavigatesAboveRoot(filter))
            {
                return NullChangeToken.Singleton;
            }

            IChangeToken changeToken = GetOrAddChangeToken(filter);


            // We made sure that browser/iOS/tvOS never uses FileSystemWatcher.
#pragma warning disable CA1416 // Validate platform compatibility
            TryEnableFileSystemWatcher();


#pragma warning restore CA1416 // Validate platform compatibility

            return changeToken;
        }

看是判斷文件是否服務要求,接着看GetOrAddChangeToken(filter);

      private IChangeToken GetOrAddChangeToken(string pattern)
        {
            IChangeToken changeToken;
            bool isWildCard = pattern.IndexOf('*') != -1;
            if (isWildCard || IsDirectoryPath(pattern))
            {
                changeToken = GetOrAddWildcardChangeToken(pattern);
            }
            else
            {
                changeToken = GetOrAddFilePathChangeToken(pattern);
            }

            return changeToken;
        }

因爲這邊操作的是文件所以看GetOrAddFilePathChangeToken(pattern)方法

        internal IChangeToken GetOrAddFilePathChangeToken(string filePath)
        {
            if (!_filePathTokenLookup.TryGetValue(filePath, out ChangeTokenInfo tokenInfo))
            {
                var cancellationTokenSource = new CancellationTokenSource();
                var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token);
                tokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken);
                tokenInfo = _filePathTokenLookup.GetOrAdd(filePath, tokenInfo);
            }

            IChangeToken changeToken = tokenInfo.ChangeToken;
            return changeToken;
        }

ok,到這裏很清晰了,在FileConfigurationProvider端注入了監聽令牌,本質就是向上述代碼中的_filePathTokenLookup實例寫入CancellationTokenSource和CancellationChangeToken實例組合,然後在PhysicalFilesWatcher實例端通過FileSystemWatcher實例註冊文件監控事件遍歷_filePathTokenLookup所有的令牌根據文件名找到指定的令牌觸發令牌,並修改Data集合.配置組件就是通過這種方式實現配置熱重載.如果不明白請參考C#下 觀察者模式的另一種實現方式IChangeToken和ChangeToken.OnChange源碼如下:

        private void ReportChangeForMatchedEntries(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                // System.IO.FileSystemWatcher may trigger events that are missing the file name,
                // which makes it appear as if the root directory is renamed or deleted. Moving the root directory
                // of the file watcher is not supported, so this type of event is ignored.
                return;
            }

            path = NormalizePath(path);
            bool matched = false;
            if (_filePathTokenLookup.TryRemove(path, out ChangeTokenInfo matchInfo))
            {
                CancelToken(matchInfo);
                matched = true;
            }

            foreach (KeyValuePair<string, ChangeTokenInfo> wildCardEntry in _wildcardTokenLookup)
            {
                PatternMatchingResult matchResult = wildCardEntry.Value.Matcher.Match(path);
                if (matchResult.HasMatches &&
                    _wildcardTokenLookup.TryRemove(wildCardEntry.Key, out matchInfo))
                {
                    CancelToken(matchInfo);
                    matched = true;
                }
            }

            if (matched)
            {
                //關閉監視
                TryDisableFileSystemWatcher();
            }
        }
        private void ReportChangeForMatchedEntries(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                // System.IO.FileSystemWatcher may trigger events that are missing the file name,
                // which makes it appear as if the root directory is renamed or deleted. Moving the root directory
                // of the file watcher is not supported, so this type of event is ignored.
                return;
            }

            path = NormalizePath(path);
            bool matched = false;
            if (_filePathTokenLookup.TryRemove(path, out ChangeTokenInfo matchInfo))
            {
                CancelToken(matchInfo);
                matched = true;
            }

            foreach (KeyValuePair<string, ChangeTokenInfo> wildCardEntry in _wildcardTokenLookup)
            {
                PatternMatchingResult matchResult = wildCardEntry.Value.Matcher.Match(path);
                if (matchResult.HasMatches &&
                    _wildcardTokenLookup.TryRemove(wildCardEntry.Key, out matchInfo))
                {
                    CancelToken(matchInfo);
                    matched = true;
                }
            }

            if (matched)
            {
                //關閉監視
                TryDisableFileSystemWatcher();
            }
        }

通過CancelToken(matchInfo)從而觸發FileConfigurationProvider實例的構造函數中注入的自定義回調,回調函數如下,

_changeTokenRegistration = ChangeToken.OnChange(
                    () => Source.FileProvider.Watch(Source.Path),
                    () =>
                    {
                        Thread.Sleep(Source.ReloadDelay);
                        Load(reload: true);
                    });
        private void Load(bool reload)
        {
            IFileInfo file = Source.FileProvider?.GetFileInfo(Source.Path);
            if (file == null || !file.Exists)
            {
                //文件加載可選或者需要reload
                if (Source.Optional || reload) // Always optional on reload
                {
                    Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                }
                else
                {
                    var error = new StringBuilder($"{Source.Path} not found");
                    if (!string.IsNullOrEmpty(file?.PhysicalPath))
                    {
                        error.Append($"{file.PhysicalPath} not expected");
                    }
                    //包裝異常並拋出 因爲
                    HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString())));
                }
            }
            else
            {

                using (Stream stream = OpenRead(file))
                {
                    try
                    {
                        Load(stream);
                    }
                    catch
                    {
                        if (reload)
                        {
                            Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                        }
                        var exception = new InvalidDataException($"{file.PhysicalPath} 加載失敗");
                        HandleException(ExceptionDispatchInfo.Capture(exception));
                    }
                }
            }
            OnReload();
        }

核心方法是Load方法,其加載了配置文件,且源碼如下:

    public class JsonConfigurationProvider : FileConfigurationProvider
    {
        public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }

        public override void Load(Stream stream)
        {
            try
            {
                Data = JsonConfigurationFileParser.Parse(stream);
            }
            catch (JsonException e)
            {
                throw new FormatException(e.Message);
            }
        }
    }

調用了System.Text.Json序列化了文件的內容,並以字典的形式輸出.並給ConfigurationProvider的Data屬性賦值至於爲什麼可以通過IConfigurationRoot拿到配置值,因爲如下代碼:

其本質就是遍歷所有的ConfigurationProvider中的Data屬性,並取到相應的值.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章