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屬性,並取到相應的值.