Asp.NetCore源码学习[1-1]:配置[Configuration]
在Asp. NetCore中,配置系统支持不同的配置源(文件、环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只有一个对象,那就是
ConfigurationRoot
。其内部维护了一个集合,用于保存各种配置源的IConfigurationProvider
。IConfigurationProvider
提供了对配置源的实际访问。当通过key去ConfigurationRoot
查找对应的Value时,实际上会通过遍历IConfigurationProvider
去查找对应的键值。 本篇文章主要描述ConfigurationRoot
对象的构建过程。
本系列源码地址
1. Asp.NetCore
入口点代码
CreateWebHostBuilder(args).Build().Run();
2. Asp.NetCore
部分源码
WebHostBuilder
内部维护了_configureAppConfigurationBuilder
字段,其类型是 Action
,该委托用于对ConfigurationBuilder
进行配置。首先在构造函数中先将环境变量的配置加载到 _config
字段中,用于设置默认监听目录为程序执行目录。CreateDefaultBuilder
方法中通过调用ConfigureAppConfiguration
方法保存委托,然后在Build
方法中构建配置系统目标类ConfigurationRoot
,最后通过单例模式注入到依赖系统中。
public class WebHostBuilder
{
private Action _configureAppConfigurationBuilder;
private IConfiguration _config;
public WebHostBuilder()
{
_hostingEnvironment = new HostingEnvironment();
///
_config = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.Build();
}
public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate)
{
_configureAppConfigurationBuilder += configureDelegate;
return this;
}
public IWebHost Build()
{
var builder = new ConfigurationBuilder();
//通过委托配置IConfigurationBuilder
_configureAppConfigurationBuilder?.Invoke(_context, builder);
//构建ConfigurationRoot
var configuration = builder.Build();
// register configuration as factory to make it dispose with the service provider
services.AddSingleton(_ => configuration);
}
}
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new WebHostBuilder();
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
//为 IConfigurationBuilder 注册配置源(JsonConfigurationSource)
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
});
return builder;
}
3. 参照以上 Asp.NetCore
代码,写静态测试方法
public class ConfigurationTest
{
public static void Run()
{
//1.实例化ConfigurationBuilder
var builder = new ConfigurationBuilder();
//2.增加配置源
builder.AddJsonFile(null, "appsettings.json", true,true);
//3.构建ConfigurationRoot对象
var configuration = builder.Build();
//观察ConfigurationRoot是否发生更改
Task.Run(() => {
ChangeToken.OnChange(() => configuration.GetReloadToken(), () => {
Console.WriteLine("Configuration has changed");
});
});
Thread.Sleep(60000);
}
}
4. 通过ConfigurationBuilder
类构建目标类ConfigurationRoot
ConfigurationBuilder
是配置系统的构建类,通过Build
方法构建配置系统的目标类ConfigurationRoot
。其维护了一个用于保存IConfigurationSource
的集合,IConfigurationSource
用于提供IConfigurationProvider
。在Build
方法中,遍历IList
构建IConfigurationProvider
对象,然后将IConfigurationProvider
集合传到ConfigurationRoot
的构造函数中。代码如下:
///
/// 配置系统构建类
///
public class ConfigurationBuilder : IConfigurationBuilder
{
/// 配置源集合
public IList Sources { get; } = new List();
/// 增加一个新的配置源
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Sources.Add(source);
return this;
}
/// 通过配置源中提供的IConfigurationProvider构建配置根对象ConfigurationRoot
public IConfigurationRoot Build()
{
var providers = new List();
foreach (var source in Sources)
{
var provider = source.Build(this);
providers.Add(provider);
}
return new ConfigurationRoot(providers);
}
}
IConfigurationSource
对象不仅仅用于创建IConfigurationProvider
,还保存了构建IConfigurationProvider
需要的依赖和配置选项。
4.1 ConfigurationRoot
类实现
该类通过IList
进行初始化。其内部维护了类型为ConfigurationReloadToken
的字段,该字段提供给外部,来进行所有配置源的监听。每个IConfigurationProvider
对象同样维护了类型为ConfigurationReloadToken
的字段。当IConfigurationProvider
监测到配置源发生更改时,更改IConfigurationProvider.IChangeToken
的状态
在构造函数中执行以下操作:
- 1 调用
IConfigurationProvider.Load()
从配置源(文件、环境变量等)加载配置项 - 2 通过
ChangeToken.OnChange()
方法 监听每个IConfigurationProvider.IChangeToken
的状态改变,当其状态发生改变时更改ConfigurationRoot.IChangeToken
的状态。(在ConfigurationRoot
外部可以通过监听IChangeToken状态的改变,得知配置源发生了改变)
///
/// 配置系统的根节点
///
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList _providers;
private readonly IList _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
///
/// 使用IConfigurationProvider集合初始化ConfigurationRoot
///
/// The s for this configuration.
public ConfigurationRoot(IList providers)
{
_providers = providers ?? throw new ArgumentNullException(nameof(providers));
_changeTokenRegistrations = new List(providers.Count);
foreach (var p in providers)
{
p.Load();
//将每个IConfigurationProvider的change token与ConfigurationRoot 的change token绑定
//当IConfigurationProvider._cts.Cancel()触发时,触发当ConfigurationRoot._cts.Cancel()
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
public IEnumerable Providers => _providers;
/// 遍历_providers来设置、获取配置项的键值对
public string this[string key]
{
get
{
for (var i = _providers.Count - 1; i >= 0; i--)
{
var provider = _providers[i];
if (provider.TryGet(key, out var value))
{
return value;
}
}
return null;
}
set
{
if (!_providers.Any())
{
throw new InvalidOperationException("Can't find any IConfigurationProvider");
}
foreach (var provider in _providers)
{
provider.Set(key, value);
}
}
}
/// 获取IChangeToken,用于供外部使用者收到配置改变的消息通知
public IChangeToken GetReloadToken() => _changeToken;
public void Reload()
{
foreach (var provider in _providers)
{
provider.Load();
}
RaiseChanged();
}
/// 生成一个新的change token,并触发ConfigurationRoot的change token(旧)状态改变
private void RaiseChanged()
{
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
///
public void Dispose()
{
// dispose change token registrations
foreach (var registration in _changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach (var provider in _providers)
{
(provider as IDisposable)?.Dispose();
}
}
}
4.2 ConfigurationReloadToken
的实现
其使用适配器模式,通过CancellationTokenSource
实现IChangeToken
接口。代码如下:
///
/// 用于发送更改通知
///
public interface IChangeToken
{
/// 指示是否发生更改
bool HasChanged { get; }
/// 指示token是否会主动调用callbacks,false的情况下:token的消费者需要轮询 HasChanged 属性检测是否发生更改
bool ActiveChangeCallbacks { get; }
/// 注册回调函数, 更改发生时(HasChanged为true),会被调用(只会被调用一次)
IDisposable RegisterChangeCallback(Action
/// 基于CancellationTokenSource实现IChangeToken接口(适配器模式)
public class ConfigurationReloadToken:IChangeToken
{
private CancellationTokenSource _cts = new CancellationTokenSource();
/// CancellationTokenSource会主动调用callbacks,所以为true
public bool ActiveChangeCallbacks => true;
public bool HasChanged => _cts.IsCancellationRequested;
public IDisposable RegisterChangeCallback(Action
4.3 简述CancellationTokenSource
对象
基于协作取消模式设计的对象,用于取消异步操作或者长时间同步操作。( .NET指南/取消托管线程)
CancellationTokenSource
对象的特点:
- 1 CancellationTokenSource.Token是值类型,传递副本
- 2 调用 CancellationTokenSource.Cancel 方法提供取消通知后,CancellationTokenSource.Token的状态发生改变,调用callbacks,并改变所有Token副本的状态
- 3 需要调用dispose释放CancellationTokenSource
- 4 多次调用CancellationTokenSource.Cancel,callbacks也只会执行一次
- 5 再CancellationTokenSource.Cancel之后,新注册的callback同样也会被执行
4.4 通过ChangeToken.OnChange
静态方法实现更改通知的持续消费
由于CancellationTokenSource.Cancel只会触发一次callbacks,需要ChangeToken.OnChange来实现持续监听取消通知。
实现原理:每次需要发生更改通知时,首先生成一个新的cts,然后改变旧的cts状态,触发回调函数,最后将新的cts与回调函数绑定。
///
/// 将changeToken消费者注册到IChangeToken的回调函数中,并实现IChangeToken状态改变的持续消费
///
public static class ChangeToken
{
/// 为changetoken生产者绑定消费者.
/// 1.在IChangeToken的状态未改变的情况下,生产者每次返回相同的IChangeToken
/// 2.状态改变时,生产者生成新的IChangeToken,消费者执行响应动作,为新的IChangeToken绑定消费者,释放旧的IChangeToken
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);
}
private class ChangeTokenRegistration : IDisposable
{
private readonly Func _changeTokenProducer;
private readonly Action _changeTokenConsumer;
private readonly TState _state;
private IDisposable _disposable;//用于保存当前正在使用的 IChangeToken
private static readonly NoopDisposable _disposedSentinel = new NoopDisposable();
public ChangeTokenRegistration(Func changeTokenProducer, Action changeTokenConsumer, TState state)
{
_changeTokenProducer = changeTokenProducer;
_changeTokenConsumer = changeTokenConsumer;
_state = state;
var token = changeTokenProducer();
RegisterChangeTokenCallback(token);
}
/// 1.先执行消费者动作,再绑定新的token,防止消费者执行并发动作
/// 2.否则的话可能出现以下情况:如果在绑定新token的回调方法后,并且在执行callback之前,新token的状态发生了改变,此时第二次callback也会执行,这样会造成callback的并发执行。
private void OnChangeTokenFired()
{
var token = _changeTokenProducer();
try
{
_changeTokenConsumer(_state);
}
finally
{
RegisterChangeTokenCallback(token);
}
}
private void RegisterChangeTokenCallback(IChangeToken token)
{
var registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration)s).OnChangeTokenFired(), this);
SetDisposable(registraton);
}
/// 1.将当前使用的IChangeToken保存到_disposable字段中
/// 2.如果本对象已经释放,立刻释放新产生的 IChangeToken
/// 3.已经失效的 IChangeToken 由于已经不被引用,等待GC自动释放(为什么不手动释放?)
private void SetDisposable(IDisposable disposable)
{
// 读取当前保存的 IChangeToken
var current = Volatile.Read(ref _disposable);
// 如果本对象已经释放,立刻释放新产生的IChangeToken
if (current == _disposedSentinel)
{
disposable.Dispose();
return;
}
// 否则更新_disposable字段,返回原值
var previous = Interlocked.CompareExchange(ref _disposable, disposable, current);
// current = 之前的 IChangeToken
if (previous == _disposedSentinel)
{
// 更新失败 说明对象已释放 previous = _disposedSentinel
// 本对象已经释放,立刻释放新产生的IChangeToken
disposable.Dispose();
}
else if (previous == current)
{
// 更新成功 previous 是之前的 IChangeToken
}
else
{
// 如果其他人为 _disposable赋值,且值不为 _disposedSentinel
// 会造成对象未释放、更新失败的情况
throw new InvalidOperationException("Somebody else set the _disposable field");
}
}
// 释放当前保存的 IChangeToken,将字段赋值为_disposedSentinel
public void Dispose()
{
Interlocked.Exchange(ref _disposable, _disposedSentinel).Dispose();
}
private class NoopDisposable : IDisposable
{
public void Dispose()
{
}
}
}
}
4.6 ChangeToken.OnChange
测试方法
该测试方法通过一个ChangeTokenProducer
类来模拟内部的状态改变。通过内部维护一个ConfigurationReloadToken
,可以向外部发出更改通知(一次性)。为了实现向外部持续发出更改通知,可以在更改ConfigurationReloadToken
状态之前,重新实例化有一个新的IChangeToken
,供外部重新绑定回调方法。
class ChangeTokenTest
{
public static void Run() {
var ctsProducer = new ChangeTokenProducer();
var subscriber = ChangeToken.OnChange(() => ctsProducer.GetReloadToken(), () =>
{
Console.WriteLine("消费者观察到改变事件");
});
Console.ReadLine();
}
///
/// 假设该类需要在内部状态发生改变时向外界发送更改通知
///
private class ChangeTokenProducer
{
// cts只能执行一次相应动作
private ConfigurationReloadToken _changetoken = new ConfigurationReloadToken();
///
/// 模拟状态改变
///
public ChangeTokenProducer()
{
Task.Run(()=> {
while (true)
{
Thread.Sleep(3000);//模拟耗时
//内部状态发生改变,通知外部
RaiseChanged();
}
});
}
public IChangeToken GetReloadToken () => _changetoken;
private void RaiseChanged() {
//产生新的cts
var previousToken = Interlocked.Exchange(ref _changetoken, new ConfigurationReloadToken());
//触发老的cts动作
//外界执行响应动作时,通过GetReloadToken()获取新的cts,执行相应动作,并重新绑定回调函数
previousToken.OnReload();
}
}
}
5. IConfigurationSource
的实现
IConfigurationSource
拥有一个实现IFileProvider
接口的类属性。默认实现为PhysicalFileProvider
类,文件监控目录默认为程序集根目录。该类提供文件的访问和监控功能。在Build
方法中实例化JsonFileConfigurationProvider
,并将自身传递进去。
在.NetCore源码中JsonConfigurationSource
是继承 抽象类FileConfigurationSource
的。此处合并了两个类的代码。
public class JsonFileConfigurationSource : IConfigurationSource
{
public IFileProvider FileProvider { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder) {
EnsureDefaults(builder);
return new JsonFileConfigurationProvider(this);
}
public void EnsureDefaults(IConfigurationBuilder builder)
{
FileProvider = FileProvider ?? builder.GetFileProvider();
}
}
public static class FileConfigurationExtensions
{
/// 获取默认的IFileProvider,root目录默认为程序集根目录(AppContext.BaseDirectory)
public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
}
}
6. IConfigurationProvider
的实现
在Core的源码中继承关系为JsonConfigurationProvider: FileConfigurationProvider:ConfigurationProvider:IConfigurationProvider
。本项目代码合并了JsonConfigurationProvider FileConfigurationProvider
这两个类
6.1 ConfigurationProvider
的实现
该类使用一个字典用于保存配置项的字符串键值对。并拥有一个类型为 ConfigurationReloadToken
的字段。在配置文件发生更改时,_reloadToken
的状态发生改变,外部可以通过观察该字段的状态来得知配置文件发生更改。
///
/// 配置提供者抽象类
///
public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
/// 初始化存储配置的字典,键值忽略大小写
protected ConfigurationProvider()
{
Data = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
/// 存储配置的键值对,protected只能在子类中访问
protected IDictionary Data { get; set; }
/// 读取键值
public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value);
/// 设置键值
public virtual void Set(string key, string value) => Data[key] = value;
/// 加载配置数据源,使用virtual修饰符,在子类中实现重写
public virtual void Load()
{ }
public IChangeToken GetReloadToken()
{
return _reloadToken;
}
///
/// 触发change token,并生成一个新的change token
///
protected void OnReload()
{
//原子操作:赋值并返回原始值
var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
}
6.2 JsonFileConfigurationProvider
的实现
该类继承于抽象类ConfigurationProvider
。
在构造函数中监听 FileProvider.Watch()
方法返回的IChangeToken,收到更改通知时执行以下两个动作,一个是重新读取文件流,加载到字典中;另一个是改变_reloadToken
的状态,用于通知外部:已经重新加载配置文件。由于在本项目中直接引用了MS的PhysicalFileProvider
,而该类监听文件返回的是微软的IChangeToken
。为了兼容项目代码,通过一个适配类来转换接口。
public class JsonFileConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly IDisposable _changeTokenRegistration;
public JsonFileConfigurationSource Source { get; }
public JsonFileConfigurationProvider(JsonFileConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Source = source;
if (Source.ReloadOnChange && Source.FileProvider != null)
{
//1.IFileProvider.Watch(string filter) 返回IChangeToken
//2.绑定IChangeToken的回调函数(1。生成新的IChangeToken 2.读取配置文件、向ConfigurationRoot传递消息)
//3.检测到文件更改时,触发回调
//4.为新的IChangeToken绑定回调函数
_changeTokenRegistration = ChangeToken.OnChange(
() => new IChangeTokenAdapter(Source.FileProvider.Watch(Source.Path)),
() => {
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
}
}
//重新加载文件并向IConfigurationRoot传递更改通知
private void Load(bool reload)
{
var file = Source.FileProvider?.GetFileInfo(Source.Path);
if (file == null || !file.Exists)
{
if (Source.Optional || reload) // Always optional on reload
{
Data = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
else
{
//处理异常
}
}
else
{
// Always create new Data on reload to drop old keys
if (reload)
{
Data = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
using (var stream = file.CreateReadStream())
{
try
{
Load(stream);
}
catch (Exception e)
{
HandleException(new FileLoadExceptionContext() { Exception = e, Provider = this, Ignore = true });
}
}
}
//触发IConfigurationProvider._cts.Cancel(),向IConfigurationRoot传递更改通知
OnReload();
}
public override void Load()
{
Load(reload: false);
}
/// 从文件流中加载数据到IConfigurationProvider的Data中
public void Load(Stream stream) {
try
{
//.NetCore3.0使用JsonDocument读取json文件,生成结构化文档:
/// [key] 节点1-1:节点2-1:节点3-1 [value] Value1
/// [key] 节点1-1:节点2-2:节点3-2 [value] Value2
/// Data = JsonConfigurationFileParser.Parse(stream);
//此处使用Newtonsoft.Json,简单的序列化为普通键值对
using (StreamReader sr = new StreamReader(stream))
{
String jsonStr = sr.ReadToEnd();
Data = Newtonsoft.Json.JsonConvert.DeserializeObject>(jsonStr);
}
}
catch (Exception e)
{
throw new FormatException("读取文件流失败", e);
}
}
public void Dispose() => Dispose(true);
/// 释放_changeTokenRegistration
protected virtual void Dispose(bool disposing)
{
_changeTokenRegistration?.Dispose();
}
}
/// 适配器类
/// 将Microsoft.Extensions.Primitives.IChangeToken转换为CoreWebApp.Primitives.IChangeToken
public class IChangeTokenAdapter : IChangeToken
{
public IChangeTokenAdapter(IChangeTokenMS msToken)
{
MsToken = msToken ?? throw new ArgumentNullException(nameof(msToken));
}
private IChangeTokenMS MsToken { get; set; }
public bool HasChanged => MsToken.HasChanged;
public bool ActiveChangeCallbacks => MsToken.ActiveChangeCallbacks;
public IDisposable RegisterChangeCallback(Action
7. ConfigurationSection
类的实现
{
"OptionV1": {
"OptionV21": "ValueV21",
"OptionV22": {
"OptionV31": "ValueV31",
"OptionV32": "ValueV32"
}
}
}
对于如上的配置文件会保存为key "OptionV1:OptionV22:OptionV31" value "ValueV31"
的格式,这样同时将节点间的层级关系也保存了下来。通过ConfigurationRoot
访问键值需要提供键的全路径。ConfigurationSection
类相当于定位了某个节点,通过ConfigurationSection
访问键值只需要通过相对路径。
结语
到此为止,ConfigurationRoot
已经构建完成,然后通过DI模块以单例模式注入到系统中。在控制器中可以通过IConfiguration
访问到所有配置源的键值对,并且当配置文件发生改变时重新加载IConfigurationProvider
。下篇文章将会讲述从如何通过强类型IOptions
访问配置项。