浅析 .Net Core中Json配置的自动更新
Pre
很早在看 Jesse 的Asp.net Core快速入門的課程的時候就了解到了在Asp .net core中,如果添加的Json配置被更改了,是支持自動重載配置的,作為一名有著嚴重"造輪子"情節(jié)的程序員,最近在折騰一個博客系統(tǒng),也想造出一個這樣能自動更新以Mysql為數(shù)據(jù)源的ConfigureSource,于是點開了AddJsonFile這個拓展函數(shù)的源碼,發(fā)現(xiàn)別有洞天,蠻有意思,本篇文章就簡單地聊一聊Json config的ReloadOnChange是如何實現(xiàn)的,在學習ReloadOnChange的過程中,我們會把Configuration也順帶撩一把?,希望對小伙伴們有所幫助.
Copy 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數(shù)據(jù)源,把reloadOnChange屬性設置為true即可實現(xiàn)當文件變更時自動更新配置,這篇博客我們首先從它的源碼簡單看一下,看完你可能還是會有點懵的,別慌,我會對這些代碼進行精簡,做個簡單的小例子,希望能對你有所幫助.
一窺源碼
AddJson
首先,我們當然是從這個我們耳熟能詳?shù)臄U展函數(shù)開始,它經(jīng)歷的演變過程如下.
Copy public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder,string path,bool optional,bool reloadOnChange){
return builder.AddJsonFile((IFileProvider) null, path, optional, reloadOnChange);
}
傳遞一個null的FileProvider給另外一個重載Addjson函數(shù).
敲黑板,Null的FileProvider很重要,后面要考?.
{
return builder.AddJsonFile((Action<JsonConfigurationSource>) (s =>
{
s.FileProvider = provider;
s.Path = path;
s.Optional = optional;
s.ReloadOnChange = reloadOnChange;
s.ResolveFileProvider();
}));
}
把傳入的參數(shù)演變成一個Action委托給JsonConfigurationSource的屬性賦值.
Copy public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource){
return builder.Add<JsonConfigurationSource>(configureSource);
}
最終調(diào)用的builder.add(action)方法.
Copy 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方法里,創(chuàng)建了一個Source實例,也就是JsonConfigurationSource實例,然后把這個實例傳為剛剛的委托,這樣一來,我們在最外面?zhèn)魅氲?#34;appsettings.json",optional:true,reloadOnChange:true參數(shù)就作用到這個示例上了.
最終,這個實例添加到builder中.那么builder又是什么?它能干什么?
ConfigurationBuild
前面提及的builder默認情況下是ConfigurationBuilder,我對它的進行了簡化,關鍵代碼如下.
Copypublic 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可以保存任何實現(xiàn)了IConfigurationSource的Source,前面聊到的JsonConfigurationSource就是實現(xiàn)了這個接口,常用的還有MemoryConfigurationSource,XmlConfigureSource,CommandLineConfigurationSource等.
另外,它有一個很重要的build方法,這個build方法在WebHostBuilder方法執(zhí)行build的時候也被調(diào)用,不要問我WebHostBuilder.builder方法什么執(zhí)行的?.
Copypublic static void Main(string[] args){
CreateWebHostBuilder(args).Build().Run();
}
在ConfigureBuilder的方法里面就調(diào)用了每個Source的Builder方法,我們剛剛傳入的是一個JsonConfigurationSource,所以我們有必要看看JsonSource的builder做了什么.
這里是不是被這些builder繞哭了? 別慌,下一篇文章中我會講解如何自定義一個ConfigureSoure,會把Congigure系列類UML類圖整理一下,應該會清晰很多.
JsonConfigurationSource
Copy public class JsonConfigurationSource : FileConfigurationSource{
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new JsonConfigurationProvider(this);
}
}
這就是JsonConfigurationSource的所有代碼,未精簡,它只實現(xiàn)了一個Build方法,在Build內(nèi),EnsureDefaults被調(diào)用,可別小看它,之前那個空的FileProvider在這里被賦值了.
Copy 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如此寵幸讓我花如此大的伏筆要強調(diào)它呢?往下看.
JsonConfigurationProvider && FileConfigurationProvider
在JsonConfigurationSource的build方法內(nèi),返回的是一個JsonConfigurationProvider實例,所以直覺告訴我,在它的構造函數(shù)內(nèi)必有貓膩?.
Copy 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的構造函數(shù).
{
Source = source;
if (Source.ReloadOnChange && Source.FileProvider != null)
{
_changeTokenRegistration = ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path),
() => {
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
}
}
真是個天才,問題就在這個構造函數(shù)里,它構造函數(shù)調(diào)用了一個ChangeToken.OnChange方法,這是實現(xiàn)ReloadOnChange的關鍵,如果你點到這里還沒有關掉,恭喜,好戲開始了.
ReloadOnChange
Talk is cheap. Show me the code (屁話少說,放碼過來).
Copy 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,就看看這兩個參數(shù)的名稱,producer,consumer,生產(chǎn)者,消費者,不知道看到這個關鍵詞想到的是什么,反正我想到的是小學時學習食物鏈時的?與?.
那么我們來看看這里的?是什么,?又是什么,還得回到FileConfigurationProvider的構造函數(shù).
可以看到生產(chǎn)者?是:
Copy() => Source.FileProvider.Watch(Source.Path)消費者?是:
Copy() => {
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
}
我們想一下,一旦有一條?跑出來,就立馬被?吃了,
那我們這里也一樣,一旦有FileProvider.Watch返回了什么東西,就會發(fā)生Load()事件來重新加載數(shù)據(jù).
?與?好理解,可是代碼就沒那么好理解了,我們通過OnChange的第一個參數(shù)Func<IChangeToken> changeTokenProducer方法知道,這里的?,其實是IChangeToken.
IChangeToken
Copy public interface IChangeToken{
bool HasChanged { get; }
bool ActiveChangeCallbacks { get; }
IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
IChangeToken的重點在于里面有個RegisterChangeCallback方法,?吃?的這件事,就發(fā)生在這回調(diào)方法里面.
我們來做個?吃?的實驗.
實驗1
Copy static void Main(){
//定義一個C:\Users\liuzh\MyBox\TestSpace目錄的FileProvider
var phyFileProvider = new PhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
//讓這個Provider開始監(jiān)聽這個目錄下的所有文件
var changeToken = phyFileProvider.Watch("*.*");
//注冊?吃?這件事到回調(diào)函數(shù)
changeToken.RegisterChangeCallback(_=> { Console.WriteLine("老鼠被蛇吃"); }, new object());
//添加一個文件到目錄
AddFileToPath();
Console.ReadKey();
}
static void AddFileToPath()
{
Console.WriteLine("老鼠出洞了");
File.Create("C:\\Users\\liuzh\\MyBox\\TestSpace\\老鼠出洞了.txt").Dispose();
}
這是運行結(jié)果
可以看到,一旦在監(jiān)聽的目錄下創(chuàng)建文件,立即觸發(fā)了執(zhí)行回調(diào)函數(shù),但是如果我們繼續(xù)手動地更改(復制)監(jiān)聽目錄中的文件,回調(diào)函數(shù)就不再執(zhí)行了.
這是因為changeToken監(jiān)聽到文件變更并觸發(fā)回調(diào)函數(shù)后,這個changeToken的使命也就完成了,要想保持一直監(jiān)聽,那么我們就在在回調(diào)函數(shù)中重新獲取token,并給新的token的回調(diào)函數(shù)注冊通用的事件,這樣就能保持一直監(jiān)聽下去了.
這也就是ChangeToken.Onchange所作的事情,我們看一下源碼.
{
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的回調(diào)函數(shù),仔細看看OnChangeTokenFired里做了什么,總體來說三步.
獲取一個新的token.
調(diào)用消費者進行消費.
給新獲取的token再次注冊一個OnChangeTokenFired的回調(diào)函數(shù).
如此周而復始~~
實驗2
既然知道了OnChange的工作方式,那么我們把實驗1的代碼修改一下.
Copy static void Main(){
var phyFileProvider = new PhysicalFileProvider("C:\\Users\\liuzh\\MyBox\\TestSpace");
ChangeToken.OnChange(() => phyFileProvider.Watch("*.*"),
() => { Console.WriteLine("老鼠被蛇吃"); });
Console.ReadKey();
}
執(zhí)行效果看一下
可以看到,只要被監(jiān)控的目錄發(fā)生了文件變化,不管是新建文件,還是修改了文件內(nèi)的內(nèi)容,都會觸發(fā)回調(diào)函數(shù),其實JsonConfig中,這個回調(diào)函數(shù)就是Load(),它負責重新加載數(shù)據(jù),可也就是為什么Asp .net core中如果把ReloadOnchang設置為true后,Json的配置一旦更新,配置就會自動重載.
PhysicalFilesWatcher
那么,為什么文件一旦變化,就會觸發(fā)ChangeToken的回調(diào)函數(shù)呢? 其實PhysicalFileProvider中調(diào)用了PhysicalFilesWatcher對文件系統(tǒng)進行監(jiān)視,觀察PhysicalFilesWatcher的構造函數(shù),可以看到PhysicalFilesWatcher需要傳入FileSystemWatcher,FileSystemWatcher是system.io下的底層IO類,在構造函數(shù)中給這個Watcher的Created,Changed,Renamed,Deleted注冊EventHandler事件,最終,在這些EventHandler中會調(diào)用ChangToken的回調(diào)函數(shù),所以文件系統(tǒng)一旦發(fā)生變更就會觸發(fā)回調(diào)函數(shù).
Copy 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));
}
蔣金楠老師有一篇優(yōu)秀的文章介紹FileProvider,有興趣的可以看一下
https://www.cnblogs.com/artech/p/net-core-file-provider-02.html.
如果你和我一樣,對源碼感興趣,可以從官方的aspnet/Extensions中下載源碼研究:https://github.com/aspnet/Extensions
在下一篇文章中,我會講解如何自定義一個以Mysql為數(shù)據(jù)源的ConfigureSoure,并實現(xiàn)自動更新功能,同時還會整理Configure相關類的UML類圖,有興趣的可以關注我以便第一時間收到下篇文章.
本文章涉及的代碼地址:https://github.com/liuzhenyulive/MiniConfiguration
原文地址:https://www.cnblogs.com/CoderAyu/p/10776845.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的浅析 .Net Core中Json配置的自动更新的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: .net core 注入机制与Autof
- 下一篇: .NET Core 迁移躺坑记续集--W
