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