欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET Core 3.0之深入源码理解Configuration(一)

程序员文章站 2022-04-09 07:52:58
Configuration总体介绍 微软在.NET Core里设计出了全新的配置体系,并以非常灵活、可扩展的方式实现。从其源码来看,其运行机制大致是,根据其Source,创建一个Builder实例,并会向其添加Provider,在我们使用配置信息的时候,会从内存中获取相应的Provider实例。 . ......

configuration总体介绍

微软在.net core里设计出了全新的配置体系,并以非常灵活、可扩展的方式实现。从其源码来看,其运行机制大致是,根据其source,创建一个builder实例,并会向其添加provider,在我们使用配置信息的时候,会从内存中获取相应的provider实例。

.net core采用了统一的调用方式来加载不同类型的配置信息,并通过统一的抽象接口iconfigurationsource对配置源进行管理,这也是刚刚所说的灵活。而其扩展性就是我们可以自己自定义新的provider实例,而不会改变其原来的调用方式。接下来的文章将会基于consul,扩展一个新的provider实例。

在asp.net core 中,我们的应用配置是基于iconfigurationprovider的键值对。 我们先看一下思维导图:

基于上图,我们可以看到主要有键值对有多种,分别是:

 环境变量

命令行参数

各种形式的配置文件

内存对象

用户自定义扩展源 

核心对象

在介绍.net core配置功能之前,先简要说明一下microsoft.extensions.configuration.abstractions,该组件抽象了.net core的配置功能,并对自定义扩展制定了新的标准。以下介绍的四个核心对象全部来自于该组件。

iconfiguration

该接口表示一组键/值应用程序配置属性,应用程序使用配置时的入口对象,.net core对其有多种扩展,其派生类包括位于统一类库的iconfigurationsection,以及microsoft.extensions.configuration类库中的configurationroot、configurationsection、iconfigurationroot。我们可以通过di获取iconfiguration实例。

它主要有以下三个方法:

  • getchildren():获取直接子配置子节
  • getreloadtoken():返回一个ichangetoken,可用于确定何时重新加载配置
  • getsection(string):获取指定键的子节点

我们来看一下源码:

   1:  /// <summary>
   2:      /// represents a set of key/value application configuration properties.
   3:      /// </summary>
   4:      public interface iconfiguration
   5:      {
   6:          /// <summary>
   7:          /// gets or sets a configuration value.
   8:          /// </summary>
   9:          /// <param name="key">the configuration key.</param>
  10:          /// <returns>the configuration value.</returns>
  11:          string this[string key] { get; set; }
  12:   
  13:          /// <summary>
  14:          /// gets a configuration sub-section with the specified key.
  15:          /// </summary>
  16:          /// <param name="key">the key of the configuration section.</param>
  17:          /// <returns>the <see cref="iconfigurationsection"/>.</returns>
  18:          /// <remarks>
  19:          ///     this method will never return <c>null</c>. if no matching sub-section is found with the specified key,
  20:          ///     an empty <see cref="iconfigurationsection"/> will be returned.
  21:          /// </remarks>
  22:          iconfigurationsection getsection(string key);
  23:   
  24:          /// <summary>
  25:          /// gets the immediate descendant configuration sub-sections.
  26:          /// </summary>
  27:          /// <returns>the configuration sub-sections.</returns>
  28:          ienumerable<iconfigurationsection> getchildren();
  29:   
  30:          /// <summary>
  31:          /// returns a <see cref="ichangetoken"/> that can be used to observe when this configuration is reloaded.
  32:          /// </summary>
  33:          /// <returns>a <see cref="ichangetoken"/>.</returns>
  34:          ichangetoken getreloadtoken();
  35:      }

通常我们要求配置文件要有足够的灵活性,尤其是我们所扩展的配置信息存放在了其他服务器,当修改的时候我们很需要一套监控功能,以及时灵活的应对配置信息的修改。现在.net core为我们提供了这样一个功能,我们只需要自定义少量代码即可完成配置信息的同步。这个方法就是getreloadtoken(),其返回值是ichangetoken。此处对配置信息的同步只做一个引子,后面的文章会详细说明。

由于configurationroot、configurationsection聚集于iconfiguration接口,此处也对这两个类进行讨论,方便我们对.net core的配置功能有个更加形象的印象。这两个接口,本质上就是.net core关于配置信息的读取方式。

xml是使用比较广泛的一种数据结构,我们在配置xml时,一般会使用根节点、父节点、子节点之类的术语,此处也一样。

configurationroot是配置的根节点,也实现了iconfigurationroot,此接口只有一个方法,其主要功能就是实现对配置信息的重新加载,另外还包括一个iconfigurationprovider类型的集合属性。其源码如下

   1:  /// <summary>
   2:  /// represents the root of an <see cref="iconfiguration"/> hierarchy.
   3:  /// </summary>
   4:  public interface iconfigurationroot : iconfiguration
   5:  {
   6:      /// <summary>
   7:      /// force the configuration values to be reloaded from the underlying <see cref="iconfigurationprovider"/>s.
   8:      /// </summary>
   9:      void reload();
  10:   
  11:      /// <summary>
  12:      /// the <see cref="iconfigurationprovider"/>s for this configuration.
  13:      /// </summary>
  14:      ienumerable<iconfigurationprovider> providers { get; }
  15:  }

下面是configurationroot关于reload()方法的实现

   1:  /// <summary>
   2:  /// force the configuration values to be reloaded from the underlying sources.
   3:  /// </summary>
   4:  public void reload()
   5:  {
   6:      foreach (var provider in _providers)
   7:      {
   8:          provider.load();
   9:      }
  10:   
  11:      raisechanged();
  12:  }

通过源码我们知道,如果调用了reload()方法,所有类型的provider都会重新加载。

前面有configurationroot表示配置的根节点,那么configurationsection则表示非跟节点,毕竟父节点、子节点都是相对,所以此处使用非根节点。configurationsection继承于iconfigurationsection,该接口只有三个只读属性,分别表示配置信息的key、value以及路径信息,需要指出的是,此处的路径信息主要指从根节点到当前节点的路径,以表示当前节点的位置,类似于a:b:c可以表示节点c的位置,其中a、b、c都是configurationsection的key。以下是configurationsection的源码

   1:  /// <summary>
   2:  /// represents a section of application configuration values.
   3:  /// </summary>
   4:  public interface iconfigurationsection : iconfiguration
   5:  {
   6:      /// <summary>
   7:      /// gets the key this section occupies in its parent.
   8:      /// </summary>
   9:      string key { get; }
  10:   
  11:      /// <summary>
  12:      /// gets the full path to this section within the <see cref="iconfiguration"/>.
  13:      /// </summary>
  14:      string path { get; }
  15:   
  16:      /// <summary>
  17:      /// gets or sets the section value.
  18:      /// </summary>
  19:      string value { get; set; }
  20:  }

iconfigurationbuilder

该接口主要用于创建iconfigurationprovider,其派生类包括microsoft.extensions.configuration.configurationbuilder。其成员包括

两个只读属性:

  • properties:获取可用于在iconfigurationbuilder之间共享数据的键/值集合
  • sources:该属性用于缓存不同的配置源,以用于相对应的provider的创建

两个方法:

  • add(iconfigurationsource source):新增iconfigurationsource,并添加到属性中sources中
  • build():该方法遍历sources属性,并调用iconfigurationsource的build()方法,通过获取provider集合,最终创建iconfigurationroot对象

configurationbuilder源码如下

   1:  /// <summary>
   2:      /// used to build key/value based configuration settings for use in an application.
   3:      /// </summary>
   4:      public class configurationbuilder : iconfigurationbuilder
   5:      {
   6:          /// <summary>
   7:          /// returns the sources used to obtain configuration values.
   8:          /// </summary>
   9:          public ilist<iconfigurationsource> sources { get; } = new list<iconfigurationsource>();
  10:   
  11:          /// <summary>
  12:          /// gets a key/value collection that can be used to share data between the <see cref="iconfigurationbuilder"/>
  13:          /// and the registered <see cref="iconfigurationprovider"/>s.
  14:          /// </summary>
  15:          public idictionary<string, object> properties { get; } = new dictionary<string, object>();
  16:   
  17:          /// <summary>
  18:          /// adds a new configuration source.
  19:          /// </summary>
  20:          /// <param name="source">the configuration source to add.</param>
  21:          /// <returns>the same <see cref="iconfigurationbuilder"/>.</returns>
  22:          public iconfigurationbuilder add(iconfigurationsource source)
  23:          {
  24:              if (source == null)
  25:              {
  26:                  throw new argumentnullexception(nameof(source));
  27:              }
  28:   
  29:              sources.add(source);
  30:              return this;
  31:          }
  32:   
  33:          /// <summary>
  34:          /// builds an <see cref="iconfiguration"/> with keys and values from the set of providers registered in
  35:          /// <see cref="sources"/>.
  36:          /// </summary>
  37:          /// <returns>an <see cref="iconfigurationroot"/> with keys and values from the registered providers.</returns>
  38:          public iconfigurationroot build()
  39:          {
  40:              var providers = new list<iconfigurationprovider>();
  41:              foreach (var source in sources)
  42:              {
  43:                  var provider = source.build(this);
  44:                  providers.add(provider);
  45:              }
  46:              return new configurationroot(providers);
  47:          }
  48:      }

此处令人感慨颇多,我们最终调用 configurationroot 的构造函数,究其原因是provider提供了统一的数据访问方式,不管是基于何种类型的provider,我们都可以调用其load()方法加载配置项。此外,iconfigurationbuilder本身有很多的扩展方法来注册数据源,比如addjsonfile()扩展方法。我们来看一下,我们常见的写法,

   1:  var builder = new configurationbuilder()
   2:   
   3:              .setbasepath(env.contentrootpath)
   4:   
   5:              .addjsonfile("appsettings1.json", false, true)
   6:   
   7:              .addjsonfile("appsettings2.json", false, true);
   8:   
   9:  configuration = builder.build();

iconfigurationsource

该接口表示应用程序配置的键值对。其派生类包括microsoft.extensions.configuration.chainedconfigurationsource、microsoft.extensions.configuration.memory.memoryconfigurationsource。另外该派生类还会在文件类配置场景下依赖microsoft.extensions.configuration.fileextensions组件。

它是所有配置源的抽象表示,包括json、xml、ini、环境变量等等。通过上文我们也知道了,iconfigurationbuilder会注册多个iconfigurationsource实例。它只有一个方法,就是build()方法,并返回iconfigurationprovider,由此可见,iconfigurationprovider的创建依赖于iconfigurationsource,这也是一一对应的关系。所有不同的源最终都会转化成统一的键值对表示。

以下为

   1:  /// <summary>
   2:  /// represents a source of configuration key/values for an application.
   3:  /// </summary>
   4:  public interface iconfigurationsource
   5:  {
   6:      /// <summary>
   7:      /// builds the <see cref="iconfigurationprovider"/> for this source.
   8:      /// </summary>
   9:      /// <param name="builder">the <see cref="iconfigurationbuilder"/>.</param>
  10:      /// <returns>an <see cref="iconfigurationprovider"/></returns>
  11:      iconfigurationprovider build(iconfigurationbuilder builder);
  12:  }

以下是memoryconfigurationsource的源码

   1:  /// <summary>
   2:  /// represents in-memory data as an <see cref="iconfigurationsource"/>.
   3:  /// </summary>
   4:  public class memoryconfigurationsource : iconfigurationsource
   5:  {
   6:      /// <summary>
   7:      /// the initial key value configuration pairs.
   8:      /// </summary>
   9:      public ienumerable<keyvaluepair<string, string>> initialdata { get; set; }
  10:   
  11:      /// <summary>
  12:      /// builds the <see cref="memoryconfigurationprovider"/> for this source.
  13:      /// </summary>
  14:      /// <param name="builder">the <see cref="iconfigurationbuilder"/>.</param>
  15:      /// <returns>a <see cref="memoryconfigurationprovider"/></returns>
  16:      public iconfigurationprovider build(iconfigurationbuilder builder)
  17:      {
  18:          return new memoryconfigurationprovider(this);
  19:      }
  20:  }

iconfigurationprovider

通过上文的介绍,我们可以知道iconfigurationprovider是统一的对外接口,对用户提供配置的查询、重新加载等功能。其派生类包括microsoft.extensions.configuration.configurationprovider、microsoft.extensions.configuration.chainedconfigurationprovider、microsoft.extensions.configuration.memory.memoryconfigurationprovider。另外该派生类还会在文件类配置场景下依赖microsoft.extensions.configuration.fileextensions组件。

以下是microsoft.extensions.configuration.configurationprovider的源码:

   1:  /// <summary>
   2:  /// base helper class for implementing an <see cref="iconfigurationprovider"/>
   3:  /// </summary>
   4:  public abstract class configurationprovider : iconfigurationprovider
   5:  {
   6:      private configurationreloadtoken _reloadtoken = new configurationreloadtoken();
   7:   
   8:      /// <summary>
   9:      /// initializes a new <see cref="iconfigurationprovider"/>
  10:      /// </summary>
  11:      protected configurationprovider()
  12:      {
  13:          data = new dictionary<string, string>(stringcomparer.ordinalignorecase);
  14:      }
  15:   
  16:      /// <summary>
  17:      /// the configuration key value pairs for this provider.
  18:      /// </summary>
  19:      protected idictionary<string, string> data { get; set; }
  20:   
  21:      /// <summary>
  22:      /// attempts to find a value with the given key, returns true if one is found, false otherwise.
  23:      /// </summary>
  24:      /// <param name="key">the key to lookup.</param>
  25:      /// <param name="value">the value found at key if one is found.</param>
  26:      /// <returns>true if key has a value, false otherwise.</returns>
  27:      public virtual bool tryget(string key, out string value)
  28:          => data.trygetvalue(key, out value);
  29:   
  30:      /// <summary>
  31:      /// sets a value for a given key.
  32:      /// </summary>
  33:      /// <param name="key">the configuration key to set.</param>
  34:      /// <param name="value">the value to set.</param>
  35:      public virtual void set(string key, string value)
  36:          => data[key] = value;
  37:   
  38:      /// <summary>
  39:      /// loads (or reloads) the data for this provider.
  40:      /// </summary>
  41:      public virtual void load()
  42:      { }
  43:     
  44:      /// <summary>
  45:      /// returns the list of keys that this provider has.
  46:      /// </summary>
  47:      /// <param name="earlierkeys">the earlier keys that other providers contain.</param>
  48:      /// <param name="parentpath">the path for the parent iconfiguration.</param>
  49:      /// <returns>the list of keys for this provider.</returns>
  50:      public virtual ienumerable<string> getchildkeys(
  51:          ienumerable<string> earlierkeys,
  52:          string parentpath)
  53:      {
  54:          var prefix = parentpath == null ? string.empty : parentpath + configurationpath.keydelimiter;
  55:   
  56:          return data
  57:              .where(kv => kv.key.startswith(prefix, stringcomparison.ordinalignorecase))
  58:              .select(kv => segment(kv.key, prefix.length))
  59:              .concat(earlierkeys)
  60:              .orderby(k => k, configurationkeycomparer.instance);
  61:      }
  62:   
  63:      private static string segment(string key, int prefixlength)
  64:      {
  65:          var indexof = key.indexof(configurationpath.keydelimiter, prefixlength, stringcomparison.ordinalignorecase);
  66:          return indexof < 0 ? key.substring(prefixlength) : key.substring(prefixlength, indexof - prefixlength);
  67:      }
  68:   
  69:      /// <summary>
  70:      /// returns a <see cref="ichangetoken"/> that can be used to listen when this provider is reloaded.
  71:      /// </summary>
  72:      /// <returns></returns>
  73:      public ichangetoken getreloadtoken()
  74:      {
  75:          return _reloadtoken;
  76:      }
  77:   
  78:      /// <summary>
  79:      /// triggers the reload change token and creates a new one.
  80:      /// </summary>
  81:      protected void onreload()
  82:      {
  83:          var previoustoken = interlocked.exchange(ref _reloadtoken, new configurationreloadtoken());
  84:          previoustoken.onreload();
  85:      }
  86:   
  87:      /// <summary>
  88:      /// generates a string representing this provider name and relevant details.
  89:      /// </summary>
  90:      /// <returns> the configuration name. </returns>
  91:      public override string tostring() => $"{gettype().name}";
  92:  }

通过源码,我们可以知道configurationprovider以字典类型缓存了多个provider对象,有需要的时候,从内存中获取即可,配置的加载通过load()方法实现,在configurationroot里我们介绍了其reload,并且说明其方法是在循环调用configurationprovider的load方法,但是此处只提供了一个虚方法,其目的是要交给其他具体的provider,比如环境变量、json、xml等,这些具体的provider可以从相应的配置源中获取配置信息。所有的子节点key通过getchildkeys方法实现,其重新加载方式通过configurationreloadtoken实例完成。

另外需要说明一下,在configurationprovider构造函数里,对字典进行了初始化,并同时设置了字典key不受大小写限制,这是一个需要注意的细节。

configuration组件结构

通过查看.net配置功能的源码,所有依赖均基于microsoft.extensions.configuration.abstractions,在其上有一层实现,即microsoft.extensions.configuration,其内部也多数是抽象实现,并提供了多个虚方法交给其派生组件,比如环境变量、命令行参数、各种文件型配置等,当然各种文件型配置还要依赖microsoft.extensions.configuration.fileextensions组件。

以下是.net core 3.0预览版里的configuration各个组件的结构图: