.Net Core Configuration源码探究
前言
上篇文章我们演示了为Configuration添加Etcd数据源,并且了解到为Configuration扩展自定义数据源还是非常简单的,核心就是把数据源的数据按照一定的规则读取到指定的字典里,这些都得益于微软设计的合理性和便捷性。本篇文章我们将一起探究Configuration源码,去了解Configuration到底是如何工作的。
ConfigurationBuilder
相信使用了.Net Core或者看过.Net Core源码的同学都非常清楚,.Net Core使用了大量的Builder模式许多核心操作都是是用来了Builder模式,微软在.Net Core使用了许多在传统.Net框架上并未使用的设计模式,这也使得.Net Core使用更方便,代码更合理。Configuration作为.Net Core的核心功能当然也不例外。
其实并没有Configuration这个类,这只是我们对配置模块的代名词。其核心是IConfiguration接口,IConfiguration又是由IConfigurationBuilder构建出来的,我们找到IConfigurationBuilder源码大致定义如下
public interface IConfigurationBuilder
{
IDictionary<string, object> Properties { get; }
IList<IConfigurationSource> Sources { get; }
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
Add方法我们上篇文章曾使用过,就是为ConfigurationBuilder添加ConfigurationSource数据源,添加的数据源被存放在Sources这个属性里。当我们要使用IConfiguration的时候通过Build的方法得到IConfiguration实例,IConfigurationRoot接口是继承自IConfiguration接口的,待会我们会探究这个接口。
我们找到IConfigurationBuilder的默认实现类ConfigurationBuilder大致代码实现如下
public class ConfigurationBuilder : IConfigurationBuilder
{
/// <summary>
/// 添加的数据源被存放到了这里
/// </summary>
public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
/// <summary>
/// 添加IConfigurationSource数据源
/// </summary>
/// <returns></returns>
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Sources.Add(source);
return this;
}
public IConfigurationRoot Build()
{
//获取所有添加的IConfigurationSource里的IConfigurationProvider
var providers = new List<IConfigurationProvider>();
foreach (var source in Sources)
{
var provider = source.Build(this);
providers.Add(provider);
}
//用providers去实例化ConfigurationRoot
return new ConfigurationRoot(providers);
}
}
这个类的定义非常的简单,相信大家都能看明白。其实整个IConfigurationBuilder的工作流程都非常简单就是将IConfigurationSource添加到Sources中,然后通过Sources里的Provider去构建IConfigurationRoot。
Configuration
通过上面我们了解到通过ConfigurationBuilder构建出来的并非是直接实现IConfiguration的实现类而是另一个接口IConfigurationRoot
ConfigurationRoot
通过源代码我们可以知道IConfigurationRoot是继承自IConfiguration,具体定义关系如下
public interface IConfigurationRoot : IConfiguration
{
/// <summary>
/// 强制刷新数据
/// </summary>
/// <returns></returns>
void Reload();
IEnumerable<IConfigurationProvider> Providers { get; }
}
public interface IConfiguration
{
string this[string key] { get; set; }
/// <summary>
/// 获取指定名称子数据节点
/// </summary>
/// <returns></returns>
IConfigurationSection GetSection(string key);
/// <summary>
/// 获取所有子数据节点
/// </summary>
/// <returns></returns>
IEnumerable<IConfigurationSection> GetChildren();
/// <summary>
/// 获取IChangeToken用于当数据源有数据变化时,通知外部使用者
/// </summary>
/// <returns></returns>
IChangeToken GetReloadToken();
}
接下来我们查看IConfigurationRoot实现类ConfigurationRoot的大致实现,代码有删减
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList<IIConfigurationProvider> _providers;
private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
//通过便利的方式调用ConfigurationProvider的Load方法,将数据加载到每个ConfigurationProvider的字典里
foreach (var p in providers)
{
p.Load();
//监听每个ConfigurationProvider的ReloadToken实现如果数据源发生变化去刷新Token通知外部发生变化
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
//// <summary>
/// 读取或设置配置相关信息
/// </summary>
public string this[string key]
{
get
{
//通过这个我们可以了解到读取的顺序取决于注册Source的顺序,采用的是后来者居上的方式
//后注册的会先被读取到,如果读取到直接return
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
{
//这里的设置只是把值放到内存中去,并不会持久化到相关数据源
foreach (var provider in _providers)
{
provider.Set(key, value);
}
}
}
public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);
public IChangeToken GetReloadToken() => _changeToken;
public IConfigurationSection GetSection(string key)
=> new ConfigurationSection(this, key);
//// <summary>
/// 手动调用该方法也可以实现强制刷新的效果
/// </summary>
public void Reload()
{
foreach (var provider in _providers)
{
provider.Load();
}
RaiseChanged();
}
//// <summary>
/// 强烈推荐不熟悉Interlocked的同学研究一下Interlocked具体用法
/// </summary>
private void RaiseChanged()
{
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
}
上面展示了ConfigurationRoot的核心实现其实主要就是两点
- 读取的方式其实是循环匹配注册进来的每个provider里的数据,是后来者居上的模式,同名key后注册进来的会先被读取到,然后直接返回
- 构造ConfigurationRoot的时候才把数据加载到内存中,而且为注册进来的每个provider设置监听回调
ConfigurationSection
其实通过上面的代码我们会产生一个疑问,获取子节点数据返回的是另一个接口类型IConfigurationSection,我们来看下具体的定义
public interface IConfigurationSection : IConfiguration
{
string Key { get; }
string Path { get; }
string Value { get; set; }
}
这个接口也是继承了IConfiguration,这就奇怪了分明只有一套配置IConfiguration,为什么还要区分IConfigurationRoot和IConfigurationSection呢?其实不难理解因为Configuration可以同时承载许多不同的配置源,而IConfigurationRoot正是表示承载所有配置信息的根节点,而配置又是可以表示层级化的一种结构,在根配置里获取下来的子节点是可以表示承载一套相关配置的另一套系统,所以单独使用IConfigurationSection去表示,会显得结构更清晰,比如我们有如下的json数据格式
{
"OrderId":"202005202220",
"Address":"银河系太阳系火星",
"Total":666.66,
"Products":[
{
"Id":1,
"Name":"果子狸",
"Price":66.6,
"Detail":{
"Color":"棕色",
"Weight":"1000g"
}
},
{
"Id":2,
"Name":"蝙蝠",
"Price":55.5,
"Detail":{
"Color":"黑色",
"Weight":"200g"
}
}
]
}
我们知道json是一个结构化的存储结构,其存储元素分为三种一是简单类型,二是对象类型,三是集合类型。但是字典是KV结构,并不存在结构化关系,在.Net Corez中配置系统是这么解决的,比如以上信息存储到字典中的结构就是这种
Key | Value |
OrderId | 202005202220 |
Address | 银河系太阳系火星 |
Products:0:Id | 1 |
Products:0:Name | 果子狸 |
Products:0:Detail:Color | 棕色 |
Products:1:Id | 2 |
Products:1:Name | 蝙蝠 |
Products:1:Detail:Weight | 200g |
如果我想获取Products节点下的第一条商品数据直接
IConfigurationSection productSection = configuration.GetSection("Products:0")
类比到这里的话根配置IConfigurationRoot里存储了订单的所有数据,获取下来的子节点IConfigurationSection表示了订单里第一个商品的信息,而这个商品也是一个完整的描述商品信息的数据系统,所以这样可以更清晰的区分Configuration的结构,我们来看一下ConfigurationSection的大致实现
public class ConfigurationSection : IConfigurationSection
{
private readonly IConfigurationRoot _root;
private readonly string _path;
private string _key;
public ConfigurationSection(IConfigurationRoot root, string path)
{
_root = root;
_path = path;
}
public string Path => _path;
public string Key
{
get
{
return _key;
}
}
public string Value
{
get
{
return _root[Path];
}
set
{
_root[Path] = value;
}
}
public string this[string key]
{
get
{
//获取当前Section下的数据其实就是组合了Path和Key
return _root[ConfigurationPath.Combine(Path, key)];
}
set
{
_root[ConfigurationPath.Combine(Path, key)] = value;
}
}
//获取当前节点下的某个子节点也是组合当前的Path和子节点的标识Key
public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
//获取当前节点下的所有子节点其实就是在字典里获取包含当前Path字符串的所有Key
public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
public IChangeToken GetReloadToken() => _root.GetReloadToken();
}
这里我们可以看到既然有Key可以获取字典里对应的Value了,为何还需要Path?通过ConfigurationRoot里的代码我们可以知道Path的初始值其实就是获取ConfigurationSection的Key,说白了其实就是如何获取到当前IConfigurationSection的路径。比如
//当前productSection的Path是 Products:0
IConfigurationSection productSection = configuration.GetSection("Products:0");
//当前productDetailSection的Path是 Products:0:Detail
IConfigurationSection productDetailSection = productSection.GetSection("Detail");
//获取到pColor的全路径就是 Products:0:Detail:Color
string pColor = productDetailSection["Color"];
而获取Section所有子节点
GetChildrenImplementation来自于IConfigurationRoot的扩展方法
internal static class InternalConfigurationRootExtensions
{
//// <summary>
/// 其实就是在数据源字典里获取Key包含给定Path的所有值
/// </summary>
internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path)
{
return root.Providers
.Aggregate(Enumerable.Empty<string>(),
(seed, source) => source.GetChildKeys(seed, path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
}
}
相信讲到这里,大家对ConfigurationSection或者是对Configuration整体的思路有一定的了解,细节上的设计确实不少。但是整体实现思路还是比较清晰的。关于Configuration还有一个比较重要的扩展方法就是将配置绑定到具体POCO的扩展方法,该方法承载在ConfigurationBinder扩展类了,由于实现比较复杂,也不是本篇文章的重点,有兴趣的同学可以自行查阅,这里就不做探究了。
总结
通过以上部分的讲解,其实我们可以大概的将Configuration配置相关总结为两大核心抽象接口IConfigurationBuilder,IConfiguration,整体结构关系可大致表示成如下关系
配置相关的整体实现思路就是IConfigurationSource作为一种特定类型的数据源,它提供了提供当前数据源的提供者ConfigurationProvider,Provider负责将数据源的数据按照一定的规则放入到字典里。IConfigurationSource添加到IConfigurationBuilder的容器中,后者使用Provide构建出整个程序的根配置容器IConfigurationRoot。通过获取IConfigurationRoot子节点得到IConfigurationSection负责维护子节点容器相关。这二者都继承自IConfiguration,然后通过他们就可以获取到整个配置体系的数据数据操作了。
以上讲解都是本人通过实践和阅读源码得出的结论,可能会存在一定的偏差或理解上的误区,但是我还是想把我的理解分享给大家,希望大家能多多包涵。如果有大家有不同的见解或者更深的理解,可以在评论区多多留言。
.Net Core Configuration源码探究的更多相关文章
- 浅谈.Net Core DependencyInjection源码探究
前言 相信使用过Asp.Net Core开发框架的人对自带的DI框架已经相当熟悉了,很多刚开始接触.Net Core的时候觉得不适应,主要就是因为Core默认集成它的原因.它是Asp.Net ...
- .NET Core Session源码探究
前言 随着互联网的兴起,技术的整体架构设计思路有了质的提升,曾经Web开发必不可少的内置对象Session已经被慢慢的遗弃.主要原因有两点,一是Session依赖Cookie存放Session ...
- .NET Core HttpClient源码探究
前言 在之前的文章我们介绍过HttpClient相关的服务发现,确实HttpClient是目前.NET Core进行Http网络编程的的主要手段.在之前的介绍中也看到了,我们使用了一个很重要的 ...
- spring-cloud-sleuth+zipkin源码探究
1. spring-cloud-sleuth+zipkin源码探究 1.1. 前言 粗略看了下spring cloud sleuth core源码,发现内容真的有点多,它支持了很多类型的链路追踪, ...
- spring-boot-2.0.3之quartz集成,数据源问题,源码探究
前言 开心一刻 着火了,他报警说:119吗,我家发生火灾了. 119问:在哪里? 他说:在我家. 119问:具体点. 他说:在我家的厨房里. 119问:我说你现在的位置. 他说:我趴在桌子底下. 11 ...
- Mybatis日志源码探究
一.项目搭建 1.pom.xml <dependencies> <dependency> <groupId>log4j</groupId> <ar ...
- ASP.NET Core 框架源码地址
ASP.NET Core 框架源码地址 https://github.com/dotnet/corefx 这个是.net core的 开源项目地址 https://github.com/aspnet ...
- Vue源码探究-全局API
Vue源码探究-全局API 本篇代码位于vue/src/core/global-api/ Vue暴露了一些全局API来强化功能开发,API的使用示例官网上都有说明,无需多言.这里主要来看一下全局API ...
- Vue源码探究-事件系统
Vue源码探究-事件系统 本篇代码位于vue/src/core/instance/events.js 紧跟着生命周期之后的就是继续初始化事件相关的属性和方法.整个事件系统的代码相对其他模块来说非常简短 ...
随机推荐
- Python每日一练(1)
这两天在做Python的每日一练,感觉收获颇丰,所以来记录分享一下,一共做了三个,涉及socket,PIL,pymysql三个库,另外终于开始了Flask框架的学习,后续也会做出一些分析 第一个是一个 ...
- static关键字修饰属性
static 静态的,可以修饰属性,方法,代码块(或初始化块) , 内部内 非static修饰的属性(实例变量):各个对象各自拥有一套各自的副本 static修饰属性(l类变量): 1.由类创建的所有 ...
- VNC远程控制,如何使用VNC远程控制来管理公司?
VNC是功能强大的远程操作软件,可以实现日常的远程连接操作:如果稍加利用,可以实现公司的日常管理:既能够节省自身的时间,还可高效的完成这个功能! 我们可以使用:服务器管理工具来进行相关的操作 一.首先 ...
- Spring boot Sample 006之spring-boot-custom-servlet
一.环境 1.1.Idea 2020.1 1.2.JDK 1.8 二.步骤 2.1.点击File -> New Project -> Spring Initializer,点击next 2 ...
- Spring boot Sample 004之spring-boot-configuration-yaml
一.环境 1.1.Idea 2020.1 1.2.JDK 1.8 二.目的 通过yaml文件配置spring boot 属性文件 三.步骤 3.1.点击File -> New Project - ...
- Java实现 LeetCode 458 可怜的小猪
458. 可怜的小猪 有 1000 只水桶,其中有且只有一桶装的含有毒药,其余装的都是水.它们从外观看起来都一样.如果小猪喝了毒药,它会在 15 分钟内死去. 问题来了,如果需要你在一小时内,弄清楚哪 ...
- Java实现 蓝桥杯VIP 算法提高 快速幂
算法提高 快速幂 时间限制:1.0s 内存限制:256.0MB 问题描述 给定A, B, P,求(A^B) mod P. 输入格式 输入共一行. 第一行有三个数,N, M, P. 输出格式 输出共一行 ...
- Java实现 LeetCode 20 有效的括号
20. 有效的括号 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效. 有效字符串需满足: 左括号必须用相同类型的右括号闭合. 左括号必须以正确的顺序闭合. ...
- Linux 源码包服务的管理
源码包安装服务的启动 使用绝对路径,调用启动脚本来启动.不同源码包的启动脚本不同,可以查看源码包的安装说明,查看启动脚本的方法 /usr/local/apache2/bin/apachectl sta ...
- Spring 源码学习 - 单例bean的实例化过程
本文作者:geek,一个聪明好学的同事 1. 简介 开发中我们常用@Commpont,@Service,@Resource等注解或者配置xml去声明一个类,使其成为spring容器中的bean,以下我 ...