.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,和常用的值类型.

posted @ 2022-04-26 20:03  郑小超  阅读(213)  评论(1编辑  收藏  举报