抽丝剥茧读源码——Microsoft.Extensions.Configuration(2)


继续抽丝剥茧

? 我们知道在使用jsonxmlini等文件类配置源时,如果更改了配置文件中的内容,程序是能够感知文件变化的。这里以json配置源为例,查看AddJsonFile这个方法的定义,我们看到在添加json配置源的时候,有这么两个参数:

AddJsonFile(this IConfigurationBuilder builder, 
            string path, bool optional, bool reloadOnChange);

optional:配置文件可选

reloadOnChange:配置文件修改时进行重新加载

? OK,动手操练操练。

test.json文件:

{
  "SectionA": "ValueA",
  "SectionB": "ValueB"
}

测试代码:

var builder = new ConfigurationBuilder().AddJsonFile("test.json",false,true);
IConfigurationRoot configurationRoot = builder.Build();
Assert.Equal("ValueA", configurationRoot["SectionA"]);
Assert.Equal("ValueB", configurationRoot["SectionB"]);
Thread.Sleep(3000);  //这时将文件中SectionA的值更改为ValueA+
Assert.Equal("ValueA+", configurationRoot["SectionA"]);

? 完美通过测试。那么,配置信息是如何监视文件变化的呢?

CancellationTokenSource的使用

? 为了弄清楚配置信息是如何监视文件变化之前,我们先看下CancellationTokenSource的简单应用。

var source = new CancellationTokenSource();
source.Token.Register(() => Console.WriteLine("This is a callback"));
if(!source.IsCancellationRequested)
{
	source.Cancel();
}
// 控制台输出 "This is a callback"

? CancellationTokenSource多用在取消线程操作中,这里使用了CancellationToken注册回调的特性,使用Cancel()方法时触发回调函数。配置文件的重加载就是通过这个原理实现的,所以在接下来IChangeToken接口的实现类中,我们就能发现CancellationTokenSource的身影。

引入FileExtension

? 目前我们引入了文件配置源,并使用json文件作为了测试文件,那我们再把之前的图再补充一下。

ConfigurationRoot

? 追溯一下我们在加载配置源时的代码。

//private readonly IList _changeTokenRegistrations;
foreach (var p in providers)
{
	p.Load();
	_changeTokenRegistrations.Add(ChangeToken.OnChange(() =>p.GetReloadToken(),
	() => RaiseChanged()));
}

? 我们看到了_changeTokenRegistrations这个对象调用了ChangeToken.OnChange()静态方法,那么这个静态方法做了什么呢?

public static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer)
{
	if (changeTokenProducer == null)
	{
		throw new ArgumentNullException(nameof(changeTokenProducer));
	}
	if (changeTokenConsumer == null)
	{
		throw new ArgumentNullException(nameof(changeTokenConsumer));
	}
	return new ChangeTokenRegistration(changeTokenProducer, callback => callback(), changeTokenConsumer);
}

? 这里引入了ChangeTokenRegistration这个类,通过类的构造函数参数(Func changeTokenProducer, Action changeTokenConsumer, TState state),我们可以看出

它就是将token的生产者changeTokenProducer和消费者changeTokenConsumer做一个绑定操作,绑定的消费函数就是ConfigurationRoot中的RaiseChanged()函数。

private void RaiseChanged()
{
	var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
	previousToken.OnReload();
}

? 通过这个函数就看到previousToken执行了OnReload()函数,这里我们再跟踪到此函数的定义时,我们就发现了这段代码。

public void OnReload() => _cts.Cancel();

? 是不是有点清晰了,但是通过CancellationTokenSource的使用我们知道,token是需要绑定callback函数的,那么这个注册是在哪里进行的呢?我们再回到ChangeTokenRegistration这个类中。

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.
	var token = _changeTokenProducer();
	try
	{
		_changeTokenConsumer(_state);
	}
	finally
	{
		// We always want to ensure the callback is registered
		RegisterChangeTokenCallback(token);
	}
}

? 在ChangeTokenRegistration的构造函数中,我们知道token注册了OnChangeTokenFired()这个函数回调,这个函数主要做了三件事情:

token生产者生产一个新的token

消费token,触发消费函数

将新的token重新注册一个到此回调函数

? 这样在token失效后又被重新注册了。

? 但是,但是,但是,重要的事情说三遍。这里还不是监视文件变化部分的原理哦,这里只是多配置源用来监视配置文件重载的,那么监视文件变化其实也是这个原理,这里理顺了,文件变化只要找到使用ChangeToken.OnChange()这个静态方法的地方就可以了,文件变化也就理解了。

FileConfigurationProvider

? 在FileConfigurationProvider类的构造函数中,我们就找到了监视文件变化的源头了。这里的消费者函数,就是Load()函数,一旦监视到文件变化就调用Load对文件进行重新加载。

if (Source.ReloadOnChange && Source.FileProvider != null)
{
	_changeTokenRegistration = ChangeToken.OnChange(
	() => Source.FileProvider.Watch(Source.Path),
	() => {
			Thread.Sleep(Source.ReloadDelay);
			Load(reload: true);
	});
}

? 这里的Watch函数用到的原理是FileSystemWatcher这个类,这里不过多阐述,大家可自行查看,它主要有以下几个事件.

public event FileSystemEventHandler Deleted

public event FileSystemEventHandler Created

public event FileSystemEventHandler Changed

public event RenamedEventHandler Renamed

public event ErrorEventHandler Error;

? 配置文件方面的原理也就逐渐清晰了,不过原理易懂,设计思想难懂,希望大家多借鉴其中的设计理念,用好别人的代码,也写好自己的代码。
个人博客:www.corecoder.cn