浅析.Net Core中Json配置的自动更新
pre
很早在看 jesse 的 asp.net core快速入门 的课程的时候就了解到了在asp .net core中,如果添加的json配置被更改了,是支持自动重载配置的,作为一名有着严重"造*"情节的程序员,最近在折腾一个博客系统,也想造出一个这样能自动更新以mysql为数据源的configuresource,于是点开了addjsonfile这个拓展函数的源码,发现别有洞天,蛮有意思,本篇文章就简单地聊一聊json config的reloadonchange是如何实现的,在学习reloadonchange的过程中,我们会把configuration也顺带撩一把:grin:,希望对小伙伴们有所帮助.
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很重要,后面要考:smile:.
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
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
方法什么执行的:joy:.
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实例,所以直觉告诉我,在它的构造函数内必有猫腻:confused:.
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,生产者,消费者,不知道看到这个关键词想到的是什么,反正我想到的是小学时学习食物链时的:snake:与:rat:.
那么我们来看看这里的:snake:是什么,:rat:又是什么,还得回到 fileconfigurationprovider
的构造函数.
可以看到生产者:rat:是:
() => source.fileprovider.watch(source.path)
消费者:snake:是:
() => { thread.sleep(source.reloaddelay); load(reload: true); }
我们想一下,一旦有一条:rat:跑出来,就立马被:snake:吃了,
那我们这里也一样,一旦有fileprovider.watch返回了什么东西,就会发生load()事件来重新加载数据.
:snake:与:rat:好理解,可是代码就没那么好理解了,我们通过 onchange
的第一个参数 func<ichangetoken> changetokenproducer
方法知道,这里的:rat:,其实是 ichangetoken
.
ichangetoken
public interface ichangetoken { bool haschanged { get; } bool activechangecallbacks { get; } idisposable registerchangecallback(action<object> callback, object state); }
ichangetoken的重点在于里面有个registerchangecallback方法,:snake:吃:rat:的这件事,就发生在这回调方法里面.
我们来做个:snake:吃:rat:的实验.
实验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
里做了什么,总体来说三步.
1.获取一个新的token.
2.调用消费者进行消费.
3.给新获取的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)); }
如果你和我一样,对源码感兴趣,可以从官方的 aspnet/extensions
中下载源码研究: https://github.com/aspnet/extensions
在下一篇文章中,我会讲解如何自定义一个以mysql为数据源的configuresoure,并实现自动更新功能,同时还会整理configure相关类的uml类图,有兴趣的可以关注我以便第一时间收到下篇文章.
本文章涉及的代码地址: https://github.com/liuzhenyulive/miniconfiguration
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: C# DataTable分页处理实例代码
下一篇: C#判断密码强度的方法