.Net Core Configuration源码探究

前言

    上篇文章咱们演示了为Configuration添加Etcd数据源,而且了解到为Configuration扩展自定义数据源仍是很是简单的,核心就是把数据源的数据按照必定的规则读取到指定的字典里,这些都得益于微软设计的合理性和便捷性。本篇文章咱们将一块儿探究Configuration源码,去了解Configuration究竟是如何工做的。html

ConfigurationBuilder

    相信使用了.Net Core或者看过.Net Core源码的同窗都很是清楚,.Net Core使用了大量的Builder模式许多核心操做都是是用来了Builder模式,微软在.Net Core使用了许多在传统.Net框架上并未使用的设计模式,这也使得.Net Core使用更方便,代码更合理。Configuration做为.Net Core的核心功能固然也不例外。
    其实并无Configuration这个类,这只是咱们对配置模块的代名词。其核心是IConfiguration接口,IConfiguration又是由IConfigurationBuilder构建出来的,咱们找到IConfigurationBuilder源码大体定义以下git

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大体代码实现以下github

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。json

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的大体实现,代码有删减ide

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的核心实现其实主要就是两点ui

  • 读取的方式实际上是循环匹配注册进来的每一个provider里的数据,是后来者居上的模式,同名key后注册进来的会先被读取到,而后直接返回
  • 构造ConfigurationRoot的时候才把数据加载到内存中,并且为注册进来的每一个provider设置监听回调

ConfigurationSection

其实经过上面的代码咱们会产生一个疑问,获取子节点数据返回的是另外一个接口类型IConfigurationSection,咱们来看下具体的定义this

public interface IConfigurationSection : IConfiguration
{
    string Key { get; }

    string Path { get; }

    string Value { get; set; }
}

这个接口也是继承了IConfiguration,这就奇怪了分明只有一套配置IConfiguration,为何还要区分IConfigurationRoot和IConfigurationSection呢?其实不难理解由于Configuration能够同时承载许多不一样的配置源,而IConfigurationRoot正是表示承载全部配置信息的根节点,而配置又是能够表示层级化的一种结构,在根配置里获取下来的子节点是能够表示承载一套相关配置的另外一套系统,因此单独使用IConfigurationSection去表示,会显得结构更清晰,好比咱们有以下的json数据格式spa

{
  "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,而后经过他们就能够获取到整个配置体系的数据数据操做了。
    以上讲解都是本人经过实践和阅读源码得出的结论,可能会存在必定的误差或理解上的误区,可是我仍是想把个人理解分享给你们,但愿你们能多多包涵。若是有你们有不一样的看法或者更深的理解,能够在评论区多多留言。

👇欢迎扫码关注个人公众号👇
相关文章
相关标签/搜索