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

Pre

很早在看 Jesse 的Asp.net Core快速入門的課程的時候就瞭解到了在Asp .net core中,若是添加的Json配置被更改了,是支持自動重載配置的,做爲一名有着嚴重"造輪子"情節的程序員,最近在折騰一個博客系統,也想造出一個這樣能自動更新以Mysql爲數據源的ConfigureSource,因而點開了AddJsonFile這個拓展函數的源碼,發現別有洞天,蠻有意思,本篇文章就簡單地聊一聊Json config的ReloadOnChange是如何實現的,在學習ReloadOnChange的過程當中,咱們會把Configuration也順帶撩一把😁,但願對小夥伴們有所幫助.html

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便可實現當文件變動時自動更新配置,這篇博客咱們首先從它的源碼簡單看一下,看完你可能仍是會有點懵的,別慌,我會對這些代碼進行精簡,作個簡單的小例子,但願能對你有所幫助.git

一窺源碼

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很重要,後面要考😄.github

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的屬性賦值.sql

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

最終調用的builder.add (action)方法. json

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又是什麼?它能幹什麼?c#

ConfigurationBuild

前面說起的builder默認狀況下是ConfigurationBuilder,我對它的進行了簡化,關鍵代碼以下.app

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等.ide

另外,它有一個很重要的build方法,這個build方法在WebHostBuilder方法執行build的時候也被調用,不要問我WebHostBuilder.builder方法什麼執行的😂.函數

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實例,因此直覺告訴我,在它的構造函數內必有貓膩😕.

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,生產者,消費者,不知道看到這個關鍵詞想到的是什麼,反正我想到的是小學時學習食物鏈時的🐍與🐀.

那麼咱們來看看這裏的🐍是什麼,🐀又是什麼,還得回到FileConfigurationProvider的構造函數.

能夠看到生產者🐀是:

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

消費者🐍是:

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

咱們想一下,一旦有一條🐀跑出來,就立馬被🐍吃了,

那咱們這裏也同樣,一旦有FileProvider.Watch返回了什麼東西,就會發生Load()事件來從新加載數據.

🐍與🐀好理解,但是代碼就沒那麼好理解了,咱們經過OnChange的第一個參數Func<IChangeToken> changeTokenProducer方法知道,這裏的🐀,實際上是IChangeToken.

IChangeToken

public interface IChangeToken
    {
        bool HasChanged { get; }

        bool ActiveChangeCallbacks { get; }

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

IChangeToken的重點在於裏面有個RegisterChangeCallback方法,🐍吃🐀的這件事,就發生在這回調方法裏面.
咱們來作個🐍吃🐀的實驗.

實驗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();
        }

這是運行結果
Result
能夠看到,一旦在監聽的目錄下建立文件,當即觸發了執行回調函數,可是若是咱們繼續手動地更改(複製)監聽目錄中的文件,回調函數就再也不執行了.

這是由於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();
        }

執行效果看一下

Result

能夠看到,只要被監控的目錄發生了文件變化,不論是新建文件,仍是修改了文件內的內容,都會觸發回調函數,其實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的回調函數,因此文件系統一旦發生變動就會觸發回調函數.

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));
    }

蔣金楠老師有一篇優秀的文章介紹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

相關文章
相關標籤/搜索