浅析.Net Core中Json配置的自动更新
Pre
很早在看Jesse的Asp.netCore快速入门的课程的时候就了解到了在Asp.netcore中,如果添加的Json配置被更改了,是支持自动重载配置的,作为一名有着严重"造轮子"情节的程序员,最近在折腾一个博客系统,也想造出一个这样能自动更新以Mysql为数据源的ConfigureSource,于是点开了AddJsonFile这个拓展函数的源码,发现别有洞天,蛮有意思,本篇文章就简单地聊一聊Jsonconfig的ReloadOnChange是如何实现的,在学习ReloadOnChange的过程中,我们会把Configuration也顺带撩一把:grin:,希望对小伙伴们有所帮助.
publicstaticIWebHostBuilderCreateWebHostBuilder(string[]args)=>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(option=>
{
option.AddJsonFile("appsettings.json",optional:true,reloadOnChange:true);
})
.UseStartup();
在Asp.netcore中如果配置了json数据源,把reloadOnChange属性设置为true即可实现当文件变更时自动更新配置,这篇博客我们首先从它的源码简单看一下,看完你可能还是会有点懵的,别慌,我会对这些代码进行精简,做个简单的小例子,希望能对你有所帮助.
一窥源码
AddJson
首先,我们当然是从这个我们耳熟能详的扩展函数开始,它经历的演变过程如下.
publicstaticIConfigurationBuilderAddJsonFile(thisIConfigurationBuilderbuilder,stringpath,booloptional,boolreloadOnChange)
{
returnbuilder.AddJsonFile((IFileProvider)null,path,optional,reloadOnChange);
}
传递一个null的FileProvider给另外一个重载Addjson函数.
敲黑板,Null的FileProvider很重要,后面要考:smile:.
publicstaticIConfigurationBuilderAddJsonFile(thisIConfigurationBuilderbuilder,IFileProviderprovider,stringpath,booloptional,boolreloadOnChange)
{
returnbuilder.AddJsonFile((Action)(s=>
{
s.FileProvider=provider;
s.Path=path;
s.Optional=optional;
s.ReloadOnChange=reloadOnChange;
s.ResolveFileProvider();
}));
}
把传入的参数演变成一个Action委托给JsonConfigurationSource的属性赋值.
publicstaticIConfigurationBuilderAddJsonFile(thisIConfigurationBuilderbuilder,ActionconfigureSource) { returnbuilder.Add (configureSource); }
最终调用的builder.add
publicstaticIConfigurationBuilderAdd(thisIConfigurationBuilderbuilder,Action configureSource)whereTSource:IConfigurationSource,new() { TSourcesource=newTSource(); if(configureSource!=null) configureSource(source); returnbuilder.Add((IConfigurationSource)source); }
在Add方法里,创建了一个Source实例,也就是JsonConfigurationSource实例,然后把这个实例传为刚刚的委托,这样一来,我们在最外面传入的"appsettings.json",optional:true,reloadOnChange:true参数就作用到这个示例上了.
最终,这个实例添加到builder中.那么builder又是什么?它能干什么?
ConfigurationBuild
前面提及的builder默认情况下是ConfigurationBuilder,我对它的进行了简化,关键代码如下.
publicclassConfigurationBuilder:IConfigurationBuilder
{
publicIListSources{get;}=newList();
publicIConfigurationBuilderAdd(IConfigurationSourcesource)
{
Sources.Add(source);
returnthis;
}
publicIConfigurationRootBuild()
{
varproviders=newList();
foreach(varsourceinSources)
{
varprovider=source.Build(this);
providers.Add(provider);
}
returnnewConfigurationRoot(providers);
}
}
可以看到,这个builder中有个集合类型的Sources,这个Sources可以保存任何实现了IConfigurationSource的Source,前面聊到的JsonConfigurationSource就是实现了这个接口,常用的还有MemoryConfigurationSource,XmlConfigureSource,CommandLineConfigurationSource等.
另外,它有一个很重要的build方法,这个build方法在WebHostBuilder方法执行build的时候也被调用,不要问我WebHostBuilder.builder方法什么执行的:joy:.
publicstaticvoidMain(string[]args)
{
CreateWebHostBuilder(args).Build().Run();
}
在ConfigureBuilder的方法里面就调用了每个Source的Builder方法,我们刚刚传入的是一个JsonConfigurationSource,所以我们有必要看看JsonSource的builder做了什么.
这里是不是被这些builder绕哭了?别慌,下一篇文章中我会讲解如何自定义一个ConfigureSoure,会把Congigure系列类UML类图整理一下,应该会清晰很多.
JsonConfigurationSource
publicclassJsonConfigurationSource:FileConfigurationSource
{
publicoverrideIConfigurationProviderBuild(IConfigurationBuilderbuilder)
{
EnsureDefaults(builder);
returnnewJsonConfigurationProvider(this);
}
}
这就是JsonConfigurationSource的所有代码,未精简,它只实现了一个Build方法,在Build内,EnsureDefaults被调用,可别小看它,之前那个空的FileProvider在这里被赋值了.
publicvoidEnsureDefaults(IConfigurationBuilderbuilder)
{
FileProvider=FileProvider??builder.GetFileProvider();
}
publicstaticIFileProviderGetFileProvider(thisIConfigurationBuilderbuilder)
{
returnnewPhysicalFileProvider(AppContext.BaseDirectory??string.Empty);
}
可以看到这个FileProvider默认情况下就是PhysicalFileProvider,为什么对这个FileProvider如此宠幸让我花如此大的伏笔要强调它呢?往下看.
JsonConfigurationProvider&&FileConfigurationProvider
在JsonConfigurationSource的build方法内,返回的是一个JsonConfigurationProvider实例,所以直觉告诉我,在它的构造函数内必有猫腻:confused:.
publicclassJsonConfigurationProvider:FileConfigurationProvider
{
publicJsonConfigurationProvider(JsonConfigurationSourcesource):base(source){}
publicoverridevoidLoad(Streamstream)
{
try{
Data=JsonConfigurationFileParser.Parse(stream);
}catch(JsonReaderExceptione)
{
thrownewFormatException(Resources.Error_JSONParseError,e);
}
}
}
看不出什么的代码,事出反常必有妖~~
看看base的构造函数.
publicFileConfigurationProvider(FileConfigurationSourcesource)
{
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
Talkischeap.Showmethecode(屁话少说,放码过来).
publicstaticclassChangeToken
{
publicstaticChangeTokenRegistrationOnChange(FuncchangeTokenProducer,ActionchangeTokenConsumer)
{
returnnewChangeTokenRegistration(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
publicinterfaceIChangeToken
{
boolHasChanged{get;}
boolActiveChangeCallbacks{get;}
IDisposableRegisterChangeCallback(Action
IChangeToken的重点在于里面有个RegisterChangeCallback方法,:snake:吃:rat:的这件事,就发生在这回调方法里面.
我们来做个:snake:吃:rat:的实验.
实验1
staticvoidMain()
{
//定义一个C:\Users\liuzh\MyBox\TestSpace目录的FileProvider
varphyFileProvider=newPhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
//让这个Provider开始监听这个目录下的所有文件
varchangeToken=phyFileProvider.Watch("*.*");
//注册