6.4 配置的同步
前面介绍配置模型的核心对象时,我们刻意回避了与配置同步相关的 API,本节专门介绍配置的同步。配置的同步涉及两个方面:第一,对原始的配置源实施监控并在其发生变化之后重新加载配置;第二,配置重新加载之后及时通知应用程序,进而使应用能够及时使用最新的配置。要了解配置同步机制的实现原理,需要先了解配置数据的流向。
6.4.1 配置数据流
通过前面的介绍,我们已经对配置模型有了充分的了解。处于核心地位的 IConfiguration Builder对象,借助注册的 IConfigurationSource对象提供的 IConfigurationProvider对象,从相应的配置源中加载数据,而各种针对 IConfigurationProvider 接口的实现,就是为了将形态各异的原始配置数据转换成配置字典。应用程序中使用的配置数据直接来源于 IConfigurationBuilder对象创建的 IConfiguration对象,那么调用定义在 IConfiguration对象上的 API获取配置数据时,配置数据究竟具有怎样的流向?
前面提及,由 ConfigurationBuilder(IConfigurationBuilder 接口的默认实现)的 Build 方法提供的IConfiguration对象是一个ConfigurationRoot对象,它代表整棵配置树,而组成这棵树的配置节则通过 ConfigurationSection对象表示。这棵由 ConfigurationRoot对象表示的配置树其实是无状态的,也就是说,无论是 ConfigurationRoot对象还是 ConfigurationSection对象,它们并没有利用某个字段存储任何配置数据。
ConfigurationRoot对象保持着对所有注册的 IConfigurationSource对象提供的 IConfiguration Provider对象的引用,当调用 ConfigurationRoot对象或者 ConfigurationSection对象相应的 API提取配置数据时,最终都会直接从这些 IConfigurationProvider 对象中提取数据。换句话说,配置数据在整个模型中只以配置字典的形式存储在IConfigurationProvider对象上面。
应用程序在读取配置时产生的数据流基本体现在图 6-15 中。下面从 Configuration Root和 ConfigurationSection这两个类型的定义来对这个数据流,以及建立在此基础上的配置同步机制做进一步介绍,但在此之前需要先介绍一个名为ConfigurationReloadToken的类型。
图6-15 配置数据流
6.4.2 ConfigurationReloadToken
ConfigurationRoot 类型和 ConfigurationSection 类型的 GetReloadToken 方法返回的IChangeToken对象都是 ConfigurationReloadToken类型。不仅如此,对于组成同一棵配置树的所有节点对应的 IConfiguration对象(ConfigurationRoot或者 ConfigurationSection)来说,它们的GetReloadToken方法返回的其实是同一个ConfigurationReloadToken对象。
另外,对于 IConfiguration接口的 GetReloadToken方法返回的 IChangeToken对象,其作用不是在配置源发生变化时向应用程序发送通知,而是通知应用程序:配置源已经发生改变,并且新的数据已经被相应的 IConfigurationProvider 对象重新加载进来。由于 Configuration Root 对象和 ConfigurationSection 对象都不维护任何数据,它们仅仅将 API 调用转移到IConfigurationProvider 对象上,所以应用程序使用原来的 IConfiguration 对象就可以获取最新的配置数据。
ConfigurationReloadToken本质上是对一个CancellationTokenSource对象的封装。从如下代码片段可以看出,ConfigurationReloadToken与第 5章介绍的 CancellationChangeToken具有类似的定义和实现。两者唯一的不同之处在于:CancellationChangeToken 对象利用创建时提供的 Cancellation TokenSource对象对外发送通知,而 ConfigurationReloadToken对象则通过调用 OnReload方法利用内置的CancellationTokenSource对象发送通知。
6.4.3 ConfigurationRoot
下面介绍由 ConfigurationBuilder对象的 Build方法直接创建的 ConfigurationRoot对象具有怎样的实现。正如前面提及的,一个 ConfigurationRoot 对象是根据一组 IConfigurationProvider对象创建的,这些IConfigurationProvider对象则由注册的IConfigurationSource对象来提供。
ConfigurationRoot的GetReloadToken方法返回的是一个ConfigurationReloadToken对象,该对象用字段_changeToken 表示。如果需要利用这个对象对外发送配置重新加载的通知,就需要调用其 OnReload 方法,由上面的代码片段可知,该方法会在 RaiseChanged 方法中被调用。由于一个 IChangeToken 对象只能发送一次通知,所以该方法还负责创建新的 ConfigurationReload Token对象并对_changeToken字段赋值。
换句话说,一旦 ConfigurationRoot 的 RaiseChanged 方法被调用,我们就可以利用其GetReloadToken方法返回的 IChangeToken对象来接收配置被重新加载的通知。通过上面提供的代码我们可以看到,RaiseChanged 方法在两个地方被调用:第一,在构造函数中调用每个IConfigurationProvider对象的 GetReloadToken方法,得到对应的 IChangeToken对象,并在它们注册的回调中调用这个方法;第二,实现的 Reload方法在依次调用每个 IConfigurationProvider对象的 Load 方法来重新加载配置数据之后,调用了 RaiseChanged 方法。按照这个逻辑,应用程序会在如下两个场景中利用ConfigurationRoot返回的IChangeToken接收配置被重新加载的通知。
● 某个 IConfigurationProvider 对象捕捉到对应配置源的改变后自动重新加载配置,并在加载完成后利用其GetReloadToken方法返回的IChangeToken发送通知。
● 显式调用ConfigurationRoot的Reload方法手动加载配置。
了解了ConfigurationRoot的GetReloadToken方法返回的是什么样的IChangeToken之后,下面介绍它的其他成员具有怎样的实现。如下面的代码片段所示,在 ConfigurationRoot 的索引定义中,它分别调用了 IConfigurationProvider 对象的 TryGet 方法和 Set 方法,根据配置字典的Key获取和设置对应的Value。
从索引的定义可以看出,ConfigurationRoot在读取Value值时针对IConfigurationProvider列表的遍历是从后往前的,这一点非常重要,因为该特性决定了 IConfigurationSource 的注册会采用“后来居上”的原则。也就是说,如果多个 IConfigurationSource配置源提供的 IConfiguration Provider 对象包含同名的配置项,后面注册的 IConfigurationSource 对象就具有更高的选择优先级,我们应该根据这个特性合理安排 IConfigurationSource 对象的注册顺序。在设置 Value 时,ConfigurationRoot对象会调用每个IConfigurationProvider对象的Set方法,这意味着新的值会被保存到所有IConfigurationProvider对象的配置字典中。
正如前面多次提到过的,通过 ConfigurationRoot 表示的配置树的所有配置节都是一个类型为 ConfigurationSection 的对象,这一点体现在实现的 GetSection 方法上。将对应的路径作为参数,可以得到组成配置树的所有配置节。
用于获取所有子配置节的 GetChildren 方法可以通过调用内部方法 GetChildren Implementation 来实现。GetChildrenImplementation 方法旨在获取配置树某个节点的所有子节点,该方法的参数表示指定节点针对配置树根的路径。当这个方法被执行时,它会以聚合的形式遍历所有的 IConfigurationProvider,并调用它们的 GetChildKeys 方法获取所有子节点的 Key,这些 Key 与当前节点的路径合并后代表子节点的路径,这些路径最终被作为参数调用 GetSection方法创建对应的配置节。
6.4.4 ConfigurationSection
如下所示的代码片段大体上体现了代表配置节的 ConfigurationSection 类型的实现逻辑。如下面的代码片段所示,一个 ConfigurationSection 对象是通过代表配置树根的 ConfigurationRoot对象和当前配置节在配置树中的路径来构建的。ConfigurationSection 的 Path 属性直接返回构建时指定的路径,而Key属性则是根据这个路径解析出来的。
如图 6-15 所示,在 ConfigurationSection 类型中实现的大部分成员都是调用 Configuration Root对象相应的 API来实现的。ConfigurationSection的索引直接调用 ConfigurationRoot的索引来获取或者设置配置字典的 Value,GetChildren方法返回的就是调用 GetChildrenImplementation方法得到的结果,而GetReloadToken方法和GetSection方法都是通过调用同名方法实现的。