重新整理 .net core 實踐篇—————配置系統之間諜[八](檔案監控)

不問前世發表於2021-06-02

前言

前文提及到了當我們的配置檔案修改了,那麼從 configurationRoot 在此讀取會讀取到新的資料,本文進行擴充套件,並從原始碼方面簡單介紹一下,下面內容和前面幾節息息相關。

正文

先看一下,如果檔案修改,那麼是否有一個回撥函式,可以回撥呢?

答案是有的:

IChangeToken IConfiguration.GetReloadToken()

這裡演示一下:

IConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.json",optional:false,reloadOnChange: true);
var configurationRoot = builder.Build();

Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]);

IChangeToken token = configurationRoot.GetReloadToken();

token.RegisterChangeCallback(state =>
{
	Console.WriteLine(configurationRoot["key1"]);
	Console.WriteLine(configurationRoot["key2"]);
},configurationRoot);

Console.ReadKey();

一開始的值是:

{
  "key1": "value1",
  "key2": "value2"
}

然後我進行了修改:

{
  "key1": "value1_change",
  "key2": "value2_change"
}

結果如下:

原始碼解讀一下為什麼這麼做,因為在我們寫程式碼中,這種監聽場景比較常見,這裡就以此為例。

如果下文如果感到有點不適,請先看一下這個:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/change-tokens?view=aspnetcore-3.1

private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
	if (providers == null)
	{
		throw new ArgumentNullException(nameof(providers));
	}

	_providers = providers;
	_changeTokenRegistrations = new List<IDisposable>(providers.Count);
	foreach (IConfigurationProvider p in providers)
	{
		p.Load();
		_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
	}
}
private void RaiseChanged()
{
	ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
	previousToken.OnReload();
}

在ConfigurationRoot例項化的時候就為每一個provider 註冊了監聽事件,同時定義了回撥事件。

然後看一下GetReloadToken:

/// <summary>
/// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public IChangeToken GetReloadToken() => _changeToken;

這裡返回了ConfigurationReloadToken,也就是獲取到監聽物件,故而我們能夠被回撥。

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/change-tokens?view=aspnetcore-3.1 中解釋的比較詳細故而不過多贅述。

那麼就來看下json配置檔案的Provider,看下其為啥能夠這麼監聽。

public class JsonConfigurationProvider : FileConfigurationProvider
{
	/// <summary>
	/// Initializes a new instance with the specified source.
	/// </summary>
	/// <param name="source">The source settings.</param>
	public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }

	/// <summary>
	/// Loads the JSON data from a stream.
	/// </summary>
	/// <param name="stream">The stream to read.</param>
	public override void Load(Stream stream)
	{
		try
		{
			Data = JsonConfigurationFileParser.Parse(stream);
		}
		catch (JsonException e)
		{
			throw new FormatException(SR.Error_JSONParseError, e);
		}
	}
}

上面的操作_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); 就能解釋的通了,原理是利用檔案系統的GetReloadToken()令牌,

只是在FileConfigurationProvider 上封裝了一層轉換。

簡單看下:FileConfigurationProvider,下面值保留了Load部分。

public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable
{
	private void Load(bool reload)
	{
		IFileInfo file = Source.FileProvider?.GetFileInfo(Source.Path);
		if (file == null || !file.Exists)
		{
			if (Source.Optional || reload) // Always optional on reload
			{
				Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			}
			else
			{
				var error = new StringBuilder($"The configuration file '{Source.Path}' was not found and is not optional.");
				if (!string.IsNullOrEmpty(file?.PhysicalPath))
				{
					error.Append($" The physical path is '{file.PhysicalPath}'.");
				}
				HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString())));
			}
		}
		else
		{
			// Always create new Data on reload to drop old keys
			if (reload)
			{
				Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			}

			static Stream OpenRead(IFileInfo fileInfo)
			{
				if (fileInfo.PhysicalPath != null)
				{
					// The default physical file info assumes asynchronous IO which results in unnecessary overhead
					// especally since the configuration system is synchronous. This uses the same settings
					// and disables async IO.
					return new FileStream(
						fileInfo.PhysicalPath,
						FileMode.Open,
						FileAccess.Read,
						FileShare.ReadWrite,
						bufferSize: 1,
						FileOptions.SequentialScan);
				}

				return fileInfo.CreateReadStream();
			}

			using Stream stream = OpenRead(file);
			try
			{
				Load(stream);
			}
			catch (Exception e)
			{
				HandleException(ExceptionDispatchInfo.Capture(e));
			}
		}
		// REVIEW: Should we raise this in the base as well / instead?
		OnReload();
	}

	/// <summary>
	/// Loads the contents of the file at <see cref="Path"/>.
	/// </summary>
	/// <exception cref="FileNotFoundException">If Optional is <c>false</c> on the source and a
	/// file does not exist at specified Path.</exception>
	public override void Load()
	{
		Load(reload: false);
	}
}

看下上面的load,上面的load就是讀取檔案,然後交由JsonConfigurationProvider 的load呼叫 Data = JsonConfigurationFileParser.Parse(stream);轉換為字典。

這就是為上文中的ConfigurationRoot 要呼叫一下load了。

上文的ConfigurationRoot 呼叫Load 部分。

foreach (IConfigurationProvider p in providers)
{
	p.Load();
	_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}

這就回到了對前面系列中的記憶體字典操作了,而FileConfigurationProvider 又繼承ConfigurationProvider。

ConfigurationProvider 程式碼如下,主要是實現IConfigurationProvider介面,很多不同的檔案配置都會用到這個,比如說ini檔案、xml檔案等等都會先轉換為字典,然後繼承ConfigurationProvider:

public abstract class ConfigurationProvider : IConfigurationProvider
{
	private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();

	/// <summary>
	/// Initializes a new <see cref="IConfigurationProvider"/>
	/// </summary>
	protected ConfigurationProvider()
	{
		Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
	}

	/// <summary>
	/// The configuration key value pairs for this provider.
	/// </summary>
	protected IDictionary<string, string> Data { get; set; }

	/// <summary>
	/// Attempts to find a value with the given key, returns true if one is found, false otherwise.
	/// </summary>
	/// <param name="key">The key to lookup.</param>
	/// <param name="value">The value found at key if one is found.</param>
	/// <returns>True if key has a value, false otherwise.</returns>
	public virtual bool TryGet(string key, out string value)
		=> Data.TryGetValue(key, out value);

	/// <summary>
	/// Sets a value for a given key.
	/// </summary>
	/// <param name="key">The configuration key to set.</param>
	/// <param name="value">The value to set.</param>
	public virtual void Set(string key, string value)
		=> Data[key] = value;

	/// <summary>
	/// Loads (or reloads) the data for this provider.
	/// </summary>
	public virtual void Load()
	{ }

	/// <summary>
	/// Returns the list of keys that this provider has.
	/// </summary>
	/// <param name="earlierKeys">The earlier keys that other providers contain.</param>
	/// <param name="parentPath">The path for the parent IConfiguration.</param>
	/// <returns>The list of keys for this provider.</returns>
	public virtual IEnumerable<string> GetChildKeys(
		IEnumerable<string> earlierKeys,
		string parentPath)
	{
		string prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter;

		return Data
			.Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
			.Select(kv => Segment(kv.Key, prefix.Length))
			.Concat(earlierKeys)
			.OrderBy(k => k, ConfigurationKeyComparer.Instance);
	}

	private static string Segment(string key, int prefixLength)
	{
		int indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase);
		return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength);
	}

	/// <summary>
	/// Returns a <see cref="IChangeToken"/> that can be used to listen when this provider is reloaded.
	/// </summary>
	/// <returns>The <see cref="IChangeToken"/>.</returns>
	public IChangeToken GetReloadToken()
	{
		return _reloadToken;
	}

	/// <summary>
	/// Triggers the reload change token and creates a new one.
	/// </summary>
	protected void OnReload()
	{
		ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
		previousToken.OnReload();
	}

	/// <summary>
	/// Generates a string representing this provider name and relevant details.
	/// </summary>
	/// <returns> The configuration name. </returns>
	public override string ToString() => $"{GetType().Name}";
}

上述就是這個框架實現檔案配置和檔案監控的大致原理了。

這裡再梳理一遍,使用JsonConfigurationFileParser.Parse 將steam流轉換成字典,利用ChangeToken 對檔案進行監聽,如果有修改從載入即可。

好了,看完原理後,我們發現是ChangeToken的監聽機制。那麼問題來了,如果你看過上述ChangeToken的連結,你會發現RegisterChangeCallback只會呼叫一次。

原理很簡單,因為這是令牌機制的,令牌過期了,那麼這個RegisterChangeCallback自然呼叫一次,因為過期只有一次。

我們可以無限套娃方式:

static void Main(string[] args)
{
	IConfigurationBuilder builder = new ConfigurationBuilder();
	// builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.dev.json", optional: false, reloadOnChange: true);
	builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.json",optional:false,reloadOnChange: true);
	var configurationRoot = builder.Build();

	Console.WriteLine(configurationRoot["key1"]);
	Console.WriteLine(configurationRoot["key2"]);

	Register(configurationRoot);

	Console.ReadKey();
}

public  static void Register(IConfigurationRoot configurationRoot)
{
	IChangeToken token = configurationRoot.GetReloadToken();

	token.RegisterChangeCallback(state =>
	{
		Console.WriteLine(configurationRoot["key1"]);
		Console.WriteLine(configurationRoot["key2"]);
		Register(configurationRoot);
	}, configurationRoot);
}

也可以這麼做,利用ChangeToken 本身的方法:

static void Main(string[] args)
{
	IConfigurationBuilder builder = new ConfigurationBuilder();
	// builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.dev.json", optional: false, reloadOnChange: true);
	builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.json",optional:false,reloadOnChange: true);
	var configurationRoot = builder.Build();

	Console.WriteLine(configurationRoot["key1"]);
	Console.WriteLine(configurationRoot["key2"]);

	ChangeToken.OnChange(configurationRoot.GetReloadToken, () =>
	{
		Console.WriteLine(configurationRoot["key1"]);
		Console.WriteLine(configurationRoot["key2"]);
	});

	Console.ReadKey();
}

這裡OnChange的原理也是套娃,我把關鍵程式碼貼一下。

public static class ChangeToken
{
	/// <summary>
	/// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
	/// </summary>
	/// <param name="changeTokenProducer">Produces the change token.</param>
	/// <param name="changeTokenConsumer">Action called when the token changes.</param>
	/// <returns></returns>
	public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
	{
		if (changeTokenProducer == null)
		{
			throw new ArgumentNullException(nameof(changeTokenProducer));
		}
		if (changeTokenConsumer == null)
		{
			throw new ArgumentNullException(nameof(changeTokenConsumer));
		}

		return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
	}
	private class ChangeTokenRegistration<TState> : IDisposable
	{
		   private void OnChangeTokenFired()
		{
			// The order here is important. We need to take the token and then apply our changes BEFORE
			// registering. This prevents us from possible having two change updates to process concurrently.
			//
			// If the token changes after we take the token, then we'll process the update immediately upon
			// registering the callback.
			IChangeToken token = _changeTokenProducer();

			try
			{
				_changeTokenConsumer(_state);
			}
			finally
			{
				// We always want to ensure the callback is registered
				RegisterChangeTokenCallback(token);
			}
		}

		private void RegisterChangeTokenCallback(IChangeToken token)
		{
			IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>)s).OnChangeTokenFired(), this);

			SetDisposable(registraton);
		}
	}
}

同樣是套娃工程,SetDisposable是關鍵,比我們自己寫要好,回收機制利用到位,有興趣可以看下。

以上只是個人整理,如有錯誤,望請指出,謝謝。

下一節配置系統之變色龍(環境配置)。

相關文章