歡迎光臨
每天分享高質量文章

淺析 .Net Core中Json配置的自動更新

Pre

很早在看 Jesse 的Asp.net Core快速入門的課程的時候就瞭解到了在Asp .net core中,如果新增的Json配置被更改了,是支援自動多載配置的,作為一名有著嚴重”造輪子”情節的程式員,最近在折騰一個部落格系統,也想造出一個這樣能自動更新以Mysql為資料源的ConfigureSource,於是點開了AddJsonFile這個拓展函式的原始碼,發現別有洞天,蠻有意思,本篇文章就簡單地聊一聊Json config的ReloadOnChange是如何實現的,在學習ReloadOnChange的過程中,我們會把Configuration也順帶撩一把?,希望對小夥伴們有所幫助.

Copy

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(option =>
{
option.AddJsonFile("appsettings.json",optional:true,reloadOnChange:true);
})
.UseStartup();

在Asp .net core中如果配置了json資料源,把reloadOnChange屬性設定為true即可實現當檔案變更時自動更新配置,這篇部落格我們首先從它的原始碼簡單看一下,看完你可能還是會有點懵的,別慌,我會對這些程式碼進行精簡,做個簡單的小例子,希望能對你有所幫助.

一窺原始碼

AddJson

首先,我們當然是從這個我們耳熟能詳的擴充套件函式開始,它經歷的演變過程如下.

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函式.
敲黑板,Null的FileProvider很重要,後面要考?.

Copy

public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder,IFileProvider provider,string path,bool optional,bool reloadOnChange)
{
return builder.AddJsonFile((Action) (s =>
{
s.FileProvider = provider;
s.Path = path;
s.Optional = optional;
s.ReloadOnChange = reloadOnChange;
s.ResolveFileProvider();
}));
}

把傳入的引數演變成一個Action委託給JsonConfigurationSource的屬性賦值.

Copy

public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action configureSource)
{
return builder.Add(configureSource);
}

最終呼叫的builder.add(action)方法.

Copy

public static IConfigurationBuilder Add(this IConfigurationBuilder builder,Action 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,我對它的進行了簡化,關鍵程式碼如下.

Copy

public class ConfigurationBuilder : IConfigurationBuilder
{
public IList Sources { get; } = new List();

public IConfigurationBuilder Add(IConfigurationSource source)
{
Sources.Add(source);
return this;
}

public IConfigurationRoot Build()
{
var providers = new List();
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方法什麼執行的?.

Copy

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

在ConfigureBuilder的方法裡面就呼叫了每個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的所有程式碼,未精簡,它只實現了一個Build方法,在Build內,EnsureDefaults被呼叫,可別小看它,之前那個空的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如此寵幸讓我花如此大的伏筆要強調它呢?往下看.

JsonConfigurationProvider && FileConfigurationProvider

在JsonConfigurationSource的build方法內,傳回的是一個JsonConfigurationProvider實體,所以直覺告訴我,在它的建構式內必有貓膩?.

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的建構式.

Copy

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 (屁話少說,放過來).

Copy

public static class ChangeToken
{
public static ChangeTokenRegistration OnChange(Func changeTokenProducer, Action changeTokenConsumer)
{
return new ChangeTokenRegistration(changeTokenProducer, callback => callback(), changeTokenConsumer);
}
}

OnChange方法裡,先不管什麼func,action,就看看這兩個引數的名稱,producer,consumer,生產者,消費者,不知道看到這個關鍵詞想到的是什麼,反正我想到的是小學時學習食物鏈時的?與?.

那麼我們來看看這裡的?是什麼,?又是什麼,還得回到FileConfigurationProvider的建構式.

可以看到生產者?是:

Copy

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

消費者?是:

Copy


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

我們想一下,一旦有一條?跑出來,就立馬被?吃了,

那我們這裡也一樣,一旦有FileProvider.Watch傳回了什麼東西,就會發生Load()事件來重新載入資料.

?與?好理解,可是程式碼就沒那麼好理解了,我們透過OnChange的第一個引數Func changeTokenProducer方法知道,這裡的?,其實是IChangeToken.

IChangeToken

Copy

public interface IChangeToken
{
bool HasChanged { get; }

bool ActiveChangeCallbacks { get; }

IDisposable RegisterChangeCallback(Action<object> callback, object state);
}

IChangeToken的重點在於裡面有個RegisterChangeCallback方法,?吃?的這件事,就發生在這回呼方法裡面.
我們來做個?吃?的實驗.

實驗1

Copy

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所作的事情,我們看一下原始碼.

Copy

public static class ChangeToken
{
public static ChangeTokenRegistration OnChange(Func changeTokenProducer, Action changeTokenConsumer)
{
return new ChangeTokenRegistration(changeTokenProducer, callback => callback(), changeTokenConsumer);
}
}
public class ChangeTokenRegistration<TAction>
{
private readonly Func _changeTokenProducer;
private readonly Action _changeTokenConsumer;
private readonly TAction _state;

public ChangeTokenRegistration(Func changeTokenProducer, Action 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的程式碼修改一下.

Copy

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,FileSystemWatchersystem.io下的底層IO類,在建構式中給這個Watcher的Created,Changed,Renamed,Deleted註冊EventHandler事件,最終,在這些EventHandler中會呼叫ChangToken的回呼函式,所以檔案系統一旦發生變更就會觸發回呼函式.

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();
this._timerFactory = (Func) (() => 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

已同步到看一看
贊(0)

分享創造快樂