经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JSON » 查看文章
浅析.Net Core中Json配置的自动更新
来源:jb51  时间:2019/4/28 10:20:11  对本文有异议

Pre

很早在看 Jesse 的 Asp.net Core快速入门 的课程的时候就了解到了在Asp .net core中,如果添加的Json配置被更改了,是支持自动重载配置的,作为一名有着严重"造轮子"情节的程序员,最近在折腾一个博客系统,也想造出一个这样能自动更新以Mysql为数据源的ConfigureSource,于是点开了AddJsonFile这个拓展函数的源码,发现别有洞天,蛮有意思,本篇文章就简单地聊一聊Json config的ReloadOnChange是如何实现的,在学习ReloadOnChange的过程中,我们会把Configuration也顺带撩一把:grin:,希望对小伙伴们有所帮助.

  1. public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
  2. WebHost.CreateDefaultBuilder(args)
  3. .ConfigureAppConfiguration(option =>
  4. {
  5. option.AddJsonFile("appsettings.json",optional:true,reloadOnChange:true);
  6. })
  7. .UseStartup<Startup>();

在Asp .net core中如果配置了json数据源,把reloadOnChange属性设置为true即可实现当文件变更时自动更新配置,这篇博客我们首先从它的源码简单看一下,看完你可能还是会有点懵的,别慌,我会对这些代码进行精简,做个简单的小例子,希望能对你有所帮助.

一窥源码

AddJson

首先,我们当然是从这个我们耳熟能详的扩展函数开始,它经历的演变过程如下.

  1. public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder,string path,bool optional,bool reloadOnChange)
  2. {
  3. return builder.AddJsonFile((IFileProvider) null, path, optional, reloadOnChange);
  4. }

传递一个null的FileProvider给另外一个重载Addjson函数.

敲黑板,Null的FileProvider很重要,后面要考:smile:.

  1. public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder,IFileProvider provider,string path,bool optional,bool reloadOnChange)
  2. {
  3. return builder.AddJsonFile((Action<JsonConfigurationSource>) (s =>
  4. {
  5. s.FileProvider = provider;
  6. s.Path = path;
  7. s.Optional = optional;
  8. s.ReloadOnChange = reloadOnChange;
  9. s.ResolveFileProvider();
  10. }));
  11. }

把传入的参数演变成一个Action委托给 JsonConfigurationSource 的属性赋值.

  1. public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource)
  2. {
  3. return builder.Add<JsonConfigurationSource>(configureSource);
  4. }

最终调用的builder.add (action)方法.

  1. public static IConfigurationBuilder Add<TSource>(this IConfigurationBuilder builder,Action<TSource> configureSource)where TSource : IConfigurationSource, new()
  2. {
  3. TSource source = new TSource();
  4. if (configureSource != null)
  5. configureSource(source);
  6. return builder.Add((IConfigurationSource) source);
  7. }

在Add方法里,创建了一个Source实例,也就是JsonConfigurationSource实例,然后把这个实例传为刚刚的委托,这样一来,我们在最外面传入的 "appsettings.json",optional:true,reloadOnChange:true 参数就作用到这个示例上了.

最终,这个实例添加到builder中.那么builder又是什么?它能干什么?

ConfigurationBuild

前面提及的builder默认情况下是 ConfigurationBuilder ,我对它的进行了简化,关键代码如下.

  1. public class ConfigurationBuilder : IConfigurationBuilder
  2. {
  3. public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
  4.  
  5. public IConfigurationBuilder Add(IConfigurationSource source)
  6. {
  7. Sources.Add(source);
  8. return this;
  9. }
  10.  
  11. public IConfigurationRoot Build()
  12. {
  13. var providers = new List<IConfigurationProvider>();
  14. foreach (var source in Sources)
  15. {
  16. var provider = source.Build(this);
  17. providers.Add(provider);
  18. }
  19. return new ConfigurationRoot(providers);
  20. }
  21. }

可以看到,这个builder中有个集合类型的Sources,这个Sources可以保存任何实现了 IConfigurationSource 的Source,前面聊到的 JsonConfigurationSource 就是实现了这个接口,常用的还有 MemoryConfigurationSource , XmlConfigureSource , CommandLineConfigurationSource 等.

另外,它有一个很重要的build方法,这个build方法在 WebHostBuilder 方法执行 build 的时候也被调用,不要问我 WebHostBuilder.builder 方法什么执行的:joy:.

  1. public static void Main(string[] args)
  2. {
  3. CreateWebHostBuilder(args).Build().Run();
  4. }

在ConfigureBuilder的方法里面就调用了每个Source的Builder方法,我们刚刚传入的是一个 JsonConfigurationSource ,所以我们有必要看看JsonSource的builder做了什么.

这里是不是被这些builder绕哭了? 别慌,下一篇文章中我会讲解如何自定义一个ConfigureSoure,会把Congigure系列类UML类图整理一下,应该会清晰很多.

JsonConfigurationSource

  1. public class JsonConfigurationSource : FileConfigurationSource
  2. {
  3. public override IConfigurationProvider Build(IConfigurationBuilder builder)
  4. {
  5. EnsureDefaults(builder);
  6. return new JsonConfigurationProvider(this);
  7. }
  8. }

这就是 JsonConfigurationSource 的所有代码,未精简,它只实现了一个Build方法,在Build内,EnsureDefaults被调用,可别小看它,之前那个空的FileProvider在这里被赋值了.

  1. public void EnsureDefaults(IConfigurationBuilder builder)
  2. {
  3. FileProvider = FileProvider ?? builder.GetFileProvider();
  4. }
  5. public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
  6. {
  7. return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
  8. }

可以看到这个FileProvider默认情况下就是 PhysicalFileProvider ,为什么对这个 FileProvider 如此宠幸让我花如此大的伏笔要强调它呢?往下看.

JsonConfigurationProvider && FileConfigurationProvider

在JsonConfigurationSource的build方法内,返回的是一个JsonConfigurationProvider实例,所以直觉告诉我,在它的构造函数内必有猫腻:confused:.

  1. public class JsonConfigurationProvider : FileConfigurationProvider
  2. {
  3. public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }
  4.  
  5. public override void Load(Stream stream)
  6. {
  7. try {
  8. Data = JsonConfigurationFileParser.Parse(stream);
  9. } catch (JsonReaderException e)
  10. {
  11. throw new FormatException(Resources.Error_JSONParseError, e);
  12. }
  13. }
  14. }

看不出什么的代码,事出反常必有妖~~

看看base的构造函数.

  1. public FileConfigurationProvider(FileConfigurationSource source)
  2. {
  3. Source = source;
  4.  
  5. if (Source.ReloadOnChange && Source.FileProvider != null)
  6. {
  7. _changeTokenRegistration = ChangeToken.OnChange(
  8. () => Source.FileProvider.Watch(Source.Path),
  9. () => {
  10. Thread.Sleep(Source.ReloadDelay);
  11. Load(reload: true);
  12. });
  13. }
  14. }

真是个天才,问题就在这个构造函数里,它构造函数调用了一个 ChangeToken.OnChange 方法,这是实现ReloadOnChange的关键,如果你点到这里还没有关掉,恭喜,好戏开始了.

ReloadOnChange

Talk is cheap. Show me the code (屁话少说,放 过来).

  1. public static class ChangeToken
  2. {
  3. public static ChangeTokenRegistration<Action> OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
  4. {
  5. return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
  6. }
  7. }

OnChange方法里,先不管什么func,action,就看看这两个参数的名称,producer,consumer,生产者,消费者,不知道看到这个关键词想到的是什么,反正我想到的是小学时学习食物链时的:snake:与:rat:.

那么我们来看看这里的:snake:是什么,:rat:又是什么,还得回到 FileConfigurationProvider 的构造函数.

可以看到生产者:rat:是:

  1. () => Source.FileProvider.Watch(Source.Path)

消费者:snake:是:

  1. () => {
  2. Thread.Sleep(Source.ReloadDelay);
  3. Load(reload: true);
  4. }

我们想一下,一旦有一条:rat:跑出来,就立马被:snake:吃了,

那我们这里也一样,一旦有FileProvider.Watch返回了什么东西,就会发生Load()事件来重新加载数据.

:snake:与:rat:好理解,可是代码就没那么好理解了,我们通过 OnChange 的第一个参数 Func<IChangeToken> changeTokenProducer 方法知道,这里的:rat:,其实是 IChangeToken .

IChangeToken

  1. public interface IChangeToken
  2. {
  3. bool HasChanged { get; }
  4.  
  5. bool ActiveChangeCallbacks { get; }
  6.  
  7. IDisposable RegisterChangeCallback(Action<object> callback, object state);
  8. }

IChangeToken的重点在于里面有个RegisterChangeCallback方法,:snake:吃:rat:的这件事,就发生在这回调方法里面.

我们来做个:snake:吃:rat:的实验.

实验1

  1. static void Main()
  2. {
  3. //定义一个C:\Users\liuzh\MyBox\TestSpace目录的FileProvider
  4. var phyFileProvider = new PhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
  5.  
  6. //让这个Provider开始监听这个目录下的所有文件
  7. var changeToken = phyFileProvider.Watch("*.*");
  8.  
  9. //注册🐍吃🐀这件事到回调函数
  10. changeToken.RegisterChangeCallback(_=> { Console.WriteLine("老鼠被蛇吃"); }, new object());
  11.  
  12. //添加一个文件到目录
  13. AddFileToPath();
  14.  
  15. Console.ReadKey();
  16.  
  17. }
  18.  
  19. static void AddFileToPath()
  20. {
  21. Console.WriteLine("老鼠出洞了");
  22. File.Create("C:\\Users\\liuzh\\MyBox\\TestSpace\\老鼠出洞了.txt").Dispose();
  23. }

这是运行结果

可以看到,一旦在监听的目录下创建文件,立即触发了执行回调函数,但是如果我们继续手动地更改(复制)监听目录中的文件,回调函数就不再执行了.

这是因为changeToken监听到文件变更并触发回调函数后,这个changeToken的使命也就完成了,要想保持一直监听,那么我们就在在回调函数中重新获取token,并给新的token的回调函数注册通用的事件,这样就能保持一直监听下去了.

这也就是ChangeToken.Onchange所作的事情,我们看一下源码.

  1. public static class ChangeToken
  2. {
  3. public static ChangeTokenRegistration<Action> OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
  4. {
  5. return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
  6. }
  7. }
  8. public class ChangeTokenRegistration<TAction>
  9. {
  10. private readonly Func<IChangeToken> _changeTokenProducer;
  11. private readonly Action<TAction> _changeTokenConsumer;
  12. private readonly TAction _state;
  13.  
  14. public ChangeTokenRegistration(Func<IChangeToken> changeTokenProducer, Action<TAction> changeTokenConsumer, TAction state)
  15. {
  16. _changeTokenProducer = changeTokenProducer;
  17. _changeTokenConsumer = changeTokenConsumer;
  18. _state = state;
  19.  
  20. var token = changeTokenProducer();
  21.  
  22. RegisterChangeTokenCallback(token);
  23. }
  24.  
  25. private void RegisterChangeTokenCallback(IChangeToken token)
  26. {
  27. token.RegisterChangeCallback(_ => OnChangeTokenFired(), this);
  28. }
  29.  
  30. private void OnChangeTokenFired()
  31. {
  32. var token = _changeTokenProducer();
  33.  
  34. try
  35. {
  36. _changeTokenConsumer(_state);
  37. }
  38. finally
  39. {
  40. // We always want to ensure the callback is registered
  41. RegisterChangeTokenCallback(token);
  42. }
  43. }
  44. }

简单来说,就是给token注册了一个 OnChangeTokenFired 的回调函数,仔细看看 OnChangeTokenFired 里做了什么,总体来说三步.

1.获取一个新的token.
2.调用消费者进行消费.
3.给新获取的token再次注册一个OnChangeTokenFired的回调函数.

如此周而复始~~

实验2

既然知道了OnChange的工作方式,那么我们把实验1的代码修改一下.

  1. static void Main()
  2. {
  3. var phyFileProvider = new PhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
  4. ChangeToken.OnChange(() => phyFileProvider.Watch("*.*"),
  5. () => { Console.WriteLine("老鼠被蛇吃"); });
  6. Console.ReadKey();
  7. }

执行效果看一下

可以看到,只要被监控的目录发生了文件变化,不管是新建文件,还是修改了文件内的内容,都会触发回调函数,其实JsonConfig中,这个回调函数就是Load(),它负责重新加载数据,可也就是为什么Asp .net core中如果把ReloadOnchang设置为true后,Json的配置一旦更新,配置就会自动重载.

PhysicalFilesWatcher

那么,为什么文件一旦变化,就会触发ChangeToken的回调函数呢? 其实 PhysicalFileProvider 中调用了 PhysicalFilesWatcher 对文件系统进行监视,观察PhysicalFilesWatcher的构造函数,可以看到 PhysicalFilesWatcher 需要传入 FileSystemWatcher , FileSystemWatchersystem.io 下的底层IO类,在构造函数中给这个Watcher的Created,Changed,Renamed,Deleted注册EventHandler事件,最终,在这些EventHandler中会调用ChangToken的回调函数,所以文件系统一旦发生变更就会触发回调函数.

  1. public PhysicalFilesWatcher(string root,FileSystemWatcher fileSystemWatcher,bool pollForChanges,ExclusionFilters filters)
  2. {
  3. this._root = root;
  4. this._fileWatcher = fileSystemWatcher;
  5. this._fileWatcher.IncludeSubdirectories = true;
  6. this._fileWatcher.Created += new FileSystemEventHandler(this.OnChanged);
  7. this._fileWatcher.Changed += new FileSystemEventHandler(this.OnChanged);
  8. this._fileWatcher.Renamed += new RenamedEventHandler(this.OnRenamed);
  9. this._fileWatcher.Deleted += new FileSystemEventHandler(this.OnChanged);
  10. this._fileWatcher.Error += new ErrorEventHandler(this.OnError);
  11. this.PollForChanges = pollForChanges;
  12. this._filters = filters;
  13. this.PollingChangeTokens = new ConcurrentDictionary<IPollingChangeToken, IPollingChangeToken>();
  14. this._timerFactory = (Func<Timer>) (() => NonCapturingTimer.Create(new TimerCallback(PhysicalFilesWatcher.RaiseChangeEvents), (object) this.PollingChangeTokens, TimeSpan.Zero, PhysicalFilesWatcher.DefaultPollingInterval));
  15. }

如果你和我一样,对源码感兴趣,可以从官方的 aspnet/Extensions 中下载源码研究: https://github.com/aspnet/Extensions

在下一篇文章中,我会讲解如何自定义一个以Mysql为数据源的ConfigureSoure,并实现自动更新功能,同时还会整理Configure相关类的UML类图,有兴趣的可以关注我以便第一时间收到下篇文章.

本文章涉及的代码地址: https://github.com/liuzhenyulive/MiniConfiguration

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号