• .net 5.0 配置文件组件之JsonProvider源码解析


    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属性,并取到相应的值.

    (3)、复杂类型示例

    调用代码如下:

        class Program
        {
            static void Main(string[] args)
            {
                var workDir = $"{Environment.CurrentDirectory}";
                var builder = new ConfigurationBuilder()
                  .SetBasePath(workDir)
                  .AddJsonFile($"test.json", optional: true, reloadOnChange: true);
    
                var root = (ConfigurationRoot)builder.Build();
                var services = new ServiceCollection();
                services.Configure<MySqlDbOptions>(root.GetSection("MySqlDbOptions"));
    
                var provider=services.BuildServiceProvider();
                var mySqlDbOptions = provider.GetRequiredService<IOptions<MySqlDbOptions>>().Value;
                Console.ReadKey();
            }
        }
    
        class MySqlDbOptions
        {
            public List<ChildOptions> Childs { get; set; }
    
            public IDictionary<string, ChildOptions> Dic { get; set; }
        }
    
        public class ChildOptions
        { 
            public int Index { get; set; }
    
            public string Name { get; set; }
        }

    json文件如下:

    {
      "MySqlDbOptions": {
        "ConnectionName": "asdasd",
        "ConnectionString": "asdasdasdas",
        "Numbers": [ 1, 2, 3 ],
        "Childs": [
          {
            "Index": 1,
            "Name": "张三"
          },
          {
            "Index": 2,
            "Name": "李四"
          }
        ],
        "Dic": [
          {
            "Index": 1,
            "Name": "张三"
          },
          {
            "Index": 1,
            "Name": "张三"
          }
        ]
    
      }
    }

     Options组件几乎兼容所有的常用集合类型包括IEnumerable,和常用的值类型.

  • 相关阅读:
    8.Nginx常用基础模块
    7.Web基础之Nginx
    6.Web基础之http协议
    5.SSH远程服务
    4.Sersync实时同步
    3.NFS 共享存储
    前端-CSS
    前端-HTTP
    MySQL-其他知识点
    MySQL-pymysql模块
  • 原文地址:https://www.cnblogs.com/GreenLeaves/p/16193119.html
Copyright © 2020-2023  润新知