Pre
很早在看 Jesse 的 ASP.NET Core 快速入门 http://video.jessetalk.cn/course/4 的课程的时候就了解到了在 Asp .net core 中, 如果添加的 JSON 配置被更改了, 是支持自动重载配置的, 作为一名有着严重 "造轮子" 情节的程序员, 最近在折腾一个博客系统, 也想造出一个这样能自动更新以 MySQL 为数据源的 ConfigureSource, 于是点开了 AddJsonFile 这个拓展函数的源码, 发现别有洞天, 蛮有意思, 本篇文章就简单地聊一聊 JSON config 的 ReloadOnChange 是如何实现的, 在学习 ReloadOnChange 的过程中, 我们会把 Configuration 也顺带撩一把, 希望对小伙伴们有所帮助.
- public static IwebHostBuilder CreateWebHostBuilder(string[] args) =>
- WebHost.CreateDefaultBuilder(args)
- .ConfigureAppConfiguration(option =>
- {
- option.AddJsonFile("appsettings.json",optional:true,reloadOnChange:true);
- })
- .UseStartup<Startup>();
在 Asp .net core 中如果配置了 JSON 数据源, 把 reloadOnChange 属性设置为 true 即可实现当文件变更时自动更新配置, 这篇博客我们首先从它的源码简单看一下, 看完你可能还是会有点懵的, 别慌, 我会对这些代码进行精简, 做个简单的小例子, 希望能对你有所帮助.
一窥源码
AddJson
首先, 我们当然是从这个我们耳熟能详的扩展函数开始, 它经历的演变过程如下.
- public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder,string path,bool optional,bool reloadOnChange)
- {
- return builder.AddJsonFile((IFileProvider) null, path, optional, reloadOnChange);
- }
传递一个 null 的 FileProvider 给另外一个重载 Addjson 函数.
敲黑板, Null 的 FileProvider 很重要, 后面要考.
- public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder,IFileProvider provider,string path,bool optional,bool reloadOnChange)
- {
- return builder.AddJsonFile((Action<JsonConfigurationSource>) (s =>
- {
- s.FileProvider = provider;
- s.Path = path;
- s.Optional = optional;
- s.ReloadOnChange = reloadOnChange;
- s.ResolveFileProvider();
- }));
- }
把传入的参数演变成一个 Action 委托给 JsonConfigurationSource 的属性赋值.
- public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource)
- {
- return builder.Add<JsonConfigurationSource>(configureSource);
- }
最终调用的 builder.add(action)方法.
- public static IConfigurationBuilder Add<TSource>(this IConfigurationBuilder builder,Action<TSource> configureSource)where TSource : IConfigurationSource, new()
- {
- TSource source = new TSource();
- if (configureSource != null)
- configureSource(source);
- return builder.Add((IConfigurationSource) source);
- }
在 Add 方法里, 创建了一个 Source 实例, 也就是 JsonConfigurationSource 实例, 然后把这个实例传为刚刚的委托, 这样一来, 我们在最外面传入的 "appsettings.json",optional:true,reloadOnChange:true 参数就作用到这个示例上了.
最终, 这个实例添加到 builder 中. 那么 builder 又是什么? 它能干什么?
ConfigurationBuild
前面提及的 builder 默认情况下是 ConfigurationBuilder, 我对它的进行了简化, 关键代码如下.
- public class ConfigurationBuilder : IConfigurationBuilder
- {
- public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
- public IConfigurationBuilder Add(IConfigurationSource source)
- {
- Sources.Add(source);
- return this;
- }
- public IConfigurationRoot Build()
- {
- var providers = new List<IConfigurationProvider>();
- foreach (var source in Sources)
- {
- var provider = source.Build(this);
- providers.Add(provider);
- }
- return new ConfigurationRoot(providers);
- }
- }
可以看到, 这个 builder 中有个集合类型的 Sources, 这个 Sources 可以保存任何实现了 IConfigurationSource 的 Source, 前面聊到的 JsonConfigurationSource 就是实现了这个接口, 常用的还有 MemoryConfigurationSource,XmlConfigureSource,CommandLineConfigurationSource 等.
另外, 它有一个很重要的 build 方法, 这个 build 方法在 WebHostBuilder 方法执行 build 的时候也被调用, 不要问我 WebHostBuilder.builder 方法什么执行的.
- public static void Main(string[] args)
- {
- CreateWebHostBuilder(args).Build().Run();
- }
在 ConfigureBuilder 的方法里面就调用了每个 Source 的 Builder 方法, 我们刚刚传入的是一个 JsonConfigurationSource, 所以我们有必要看看 JsonSource 的 builder 做了什么.
这里是不是被这些 builder 绕哭了? 别慌, 下一篇文章中我会讲解如何自定义一个 ConfigureSoure, 会把 Congigure 系列类 UML 类图整理一下, 应该会清晰很多.
- JsonConfigurationSource
- public class JsonConfigurationSource : FileConfigurationSource
- {
- public override IConfigurationProvider Build(IConfigurationBuilder builder)
- {
- EnsureDefaults(builder);
- return new JsonConfigurationProvider(this);
- }
- }
这就是 JsonConfigurationSource 的所有代码, 未精简, 它只实现了一个 Build 方法, 在 Build 内, EnsureDefaults 被调用, 可别小看它, 之前那个空的 FileProvider 在这里被赋值了.
- public void EnsureDefaults(IConfigurationBuilder builder)
- {
- FileProvider = FileProvider ?? builder.GetFileProvider();
- }
- public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
- {
- return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
- }
可以看到这个 FileProvider 默认情况下就是 PhysicalFileProvider, 为什么对这个 FileProvider 如此宠幸让我花如此大的伏笔要强调它呢? 往下看.
JsonConfigurationProvider && FileConfigurationProvider
在 JsonConfigurationSource 的 build 方法内, 返回的是一个 JsonConfigurationProvider 实例, 所以直觉告诉我, 在它的构造函数内必有猫腻.
- public class JsonConfigurationProvider : FileConfigurationProvider
- {
- public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }
- public override void Load(Stream stream)
- {
- try {
- Data = JsonConfigurationFileParser.Parse(stream);
- } catch (JsonReaderException e)
- {
- throw new FormatException(Resources.Error_JSONParseError, e);
- }
- }
- }
看不出什么的代码, 事出反常必有妖~~
看看 base 的构造函数.
- public FileConfigurationProvider(FileConfigurationSource source)
- {
- Source = source;
- if (Source.ReloadOnChange && Source.FileProvider != null)
- {
- _changeTokenRegistration = ChangeToken.OnChange(
- () => Source.FileProvider.Watch(Source.Path),
- () => {
- Thread.Sleep(Source.ReloadDelay);
- Load(reload: true);
- });
- }
- }
真是个天才, 问题就在这个构造函数里, 它构造函数调用了一个 ChangeToken.OnChange 方法, 这是实现 ReloadOnChange 的关键, 如果你点到这里还没有关掉, 恭喜, 好戏开始了.
ReloadOnChange
Talk is cheap. Show me the code (屁话少说, 放码过来).
- public static class ChangeToken
- {
- public static ChangeTokenRegistration<Action> OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
- {
- return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
- }
- }
OnChange 方法里, 先不管什么 func,action, 就看看这两个参数的名称, producer,consumer, 生产者, 消费者, 不知道看到这个关键词想到的是什么, 反正我想到的是小学时学习食物链时的与.
那么我们来看看这里的是什么,又是什么, 还得回到 FileConfigurationProvider 的构造函数.
可以看到生产者是:
() => Source.FileProvider.Watch(Source.Path)
消费者是:
- () => {
- Thread.Sleep(Source.ReloadDelay);
- Load(reload: true);
- }
我们想一下, 一旦有一条跑出来, 就立马被吃了,
那我们这里也一样, 一旦有 FileProvider.Watch 返回了什么东西, 就会发生 Load()事件来重新加载数据.
与好理解, 可是代码就没那么好理解了, 我们通过 OnChange 的第一个参数 Func<IChangeToken> changeTokenProducer 方法知道, 这里的, 其实是 IChangeToken.
- IChangeToken
- public interface IChangeToken
- {
- bool HasChanged { get; }
- bool ActiveChangeCallbacks { get; }
- IDisposable RegisterChangeCallback(Action<object> callback, object state);
- }
IChangeToken 的重点在于里面有个 RegisterChangeCallback 方法,吃的这件事, 就发生在这回调方法里面.
我们来做个吃的实验.
实验 1
- static void Main()
- {
- // 定义一个 C:\Users\liuzh\MyBox\TestSpace 目录的 FileProvider
- var phyFileProvider = new PhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
- // 让这个 Provider 开始监听这个目录下的所有文件
- var changeToken = phyFileProvider.Watch("*.*");
- // 注册吃这件事到回调函数
- changeToken.RegisterChangeCallback(_=> { Console.WriteLine("老鼠被蛇吃"); }, new object());
- // 添加一个文件到目录
- AddFileToPath();
- Console.ReadKey();
- }
- static void AddFileToPath()
- {
- Console.WriteLine("老鼠出洞了");
- File.Create("C:\\Users\\liuzh\\MyBox\\TestSpace\\ 老鼠出洞了. txt").Dispose();
- }
这是运行结果
可以看到, 一旦在监听的目录下创建文件, 立即触发了执行回调函数, 但是如果我们继续手动地更改 (复制) 监听目录中的文件, 回调函数就不再执行了.
这是因为 changeToken 监听到文件变更并触发回调函数后, 这个 changeToken 的使命也就完成了, 要想保持一直监听, 那么我们就在在回调函数中重新获取 token, 并给新的 token 的回调函数注册通用的事件, 这样就能保持一直监听下去了.
这也就是 ChangeToken.Onchange 所作的事情, 我们看一下源码.
- public static class ChangeToken
- {
- public static ChangeTokenRegistration<Action> OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
- {
- return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
- }
- }
- public class ChangeTokenRegistration<TAction>
- {
- private readonly Func<IChangeToken> _changeTokenProducer;
- private readonly Action<TAction> _changeTokenConsumer;
- private readonly TAction _state;
- public ChangeTokenRegistration(Func<IChangeToken> changeTokenProducer, Action<TAction> changeTokenConsumer, TAction state)
- {
- _changeTokenProducer = changeTokenProducer;
- _changeTokenConsumer = changeTokenConsumer;
- _state = state;
- var token = changeTokenProducer();
- RegisterChangeTokenCallback(token);
- }
- private void RegisterChangeTokenCallback(IChangeToken token)
- {
- token.RegisterChangeCallback(_ => OnChangeTokenFired(), this);
- }
- private void OnChangeTokenFired()
- {
- var token = _changeTokenProducer();
- try
- {
- _changeTokenConsumer(_state);
- }
- finally
- {
- // We always want to ensure the callback is registered
- RegisterChangeTokenCallback(token);
- }
- }
- }
简单来说, 就是给 token 注册了一个 OnChangeTokenFired 的回调函数, 仔细看看 OnChangeTokenFired 里做了什么, 总体来说三步.
获取一个新的 token.
调用消费者进行消费.
给新获取的 token 再次注册一个 OnChangeTokenFired 的回调函数.
如此周而复始~~
实验 2
既然知道了 OnChange 的工作方式, 那么我们把实验 1 的代码修改一下.
- static void Main()
- {
- var phyFileProvider = new PhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
- ChangeToken.OnChange(() => phyFileProvider.Watch("*.*"),
- () => { Console.WriteLine("老鼠被蛇吃"); });
- Console.ReadKey();
- }
执行效果看一下
可以看到, 只要被监控的目录发生了文件变化, 不管是新建文件, 还是修改了文件内的内容, 都会触发回调函数, 其实 JsonConfig 中, 这个回调函数就是 Load(), 它负责重新加载数据, 可也就是为什么 Asp .net core 中如果把 ReloadOnchang 设置为 true 后, JSON 的配置一旦更新, 配置就会自动重载.
PhysicalFilesWatcher
那么, 为什么文件一旦变化, 就会触发 ChangeToken 的回调函数呢? 其实 PhysicalFileProvider 中调用了 PhysicalFilesWatcher 对文件系统进行监视, 观察 PhysicalFilesWatcher 的构造函数, 可以看到 PhysicalFilesWatcher 需要传入 FileSystemWatcher,FileSystemWatcher 是 system.io 下的底层 IO 类, 在构造函数中给这个 Watcher 的 Created,Changed,Renamed,Deleted 注册 EventHandler 事件, 最终, 在这些 EventHandler 中会调用 ChangToken 的回调函数, 所以文件系统一旦发生变更就会触发回调函数.
- public PhysicalFilesWatcher(string root,FileSystemWatcher fileSystemWatcher,bool pollForChanges,ExclusionFilters filters)
- {
- this._root = root;
- this._fileWatcher = fileSystemWatcher;
- this._fileWatcher.IncludeSubdirectories = true;
- this._fileWatcher.Created += new FileSystemEventHandler(this.OnChanged);
- this._fileWatcher.Changed += new FileSystemEventHandler(this.OnChanged);
- this._fileWatcher.Renamed += new RenamedEventHandler(this.OnRenamed);
- this._fileWatcher.Deleted += new FileSystemEventHandler(this.OnChanged);
- this._fileWatcher.Error += new ErrorEventHandler(this.OnError);
- this.PollForChanges = pollForChanges;
- this._filters = filters;
- this.PollingChangeTokens = new ConcurrentDictionary<IPollingChangeToken, IPollingChangeToken>();
- this._timerFactory = (Func<Timer>) (() => NonCapturingTimer.Create(new TimerCallback(PhysicalFilesWatcher.RaiseChangeEvents), (object) this.PollingChangeTokens, TimeSpan.Zero, PhysicalFilesWatcher.DefaultPollingInterval));
- }
蒋金楠老师有一篇优秀的文章介绍 FileProvider, 有兴趣的可以看一下
https://www.cnblogs.com/artech/p/net-core-file-provider-02.html.
如果你和我一样, 对源码感兴趣, 可以从官方的 aspnet/Extensions 中下载源码研究: https://github.com/aspnet/Extensions
在下一篇文章中, 我会讲解如何自定义一个以 MySQL 为数据源的 ConfigureSoure, 并实现自动更新功能, 同时还会整理 Configure 相关类的 UML 类图, 有兴趣的可以关注我以便第一时间收到下篇文章.
本文章涉及的代码地址: https://github.com/liuzhenyulive/MiniConfiguration
来源: https://www.cnblogs.com/CoderAyu/p/10776845.html