前言

前文提及到了当我们的配置文件修改了,那么从 configurationRoot 在此读取会读取到新的数据,本文进行扩展,并从源码方面简单介绍一下,下面内容和前面几节息息相关。

正文

先看一下,如果文件修改,那么是否有一个回调函数,可以回调呢?

答案是有的:

IChangeToken IConfiguration.GetReloadToken()

这里演示一下:

IConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.json",optional:false,reloadOnChange: true);
var configurationRoot = builder.Build(); Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]); IChangeToken token = configurationRoot.GetReloadToken(); token.RegisterChangeCallback(state =>
{
Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]);
},configurationRoot); Console.ReadKey();

一开始的值是:

{
"key1": "value1",
"key2": "value2"
}

然后我进行了修改:

{
"key1": "value1_change",
"key2": "value2_change"
}

结果如下:

源码解读一下为什么这么做,因为在我们写代码中,这种监听场景比较常见,这里就以此为例。

如果下文如果感到有点不适,请先看一下这个:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/change-tokens?view=aspnetcore-3.1

private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
if (providers == null)
{
throw new ArgumentNullException(nameof(providers));
} _providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
private void RaiseChanged()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}

在ConfigurationRoot实例化的时候就为每一个provider 注册了监听事件,同时定义了回调事件。

然后看一下GetReloadToken:

/// <summary>
/// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public IChangeToken GetReloadToken() => _changeToken;

这里返回了ConfigurationReloadToken,也就是获取到监听对象,故而我们能够被回调。

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/change-tokens?view=aspnetcore-3.1 中解释的比较详细故而不过多赘述。

那么就来看下json配置文件的Provider,看下其为啥能够这么监听。

public class JsonConfigurationProvider : FileConfigurationProvider
{
/// <summary>
/// Initializes a new instance with the specified source.
/// </summary>
/// <param name="source">The source settings.</param>
public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { } /// <summary>
/// Loads the JSON data from a stream.
/// </summary>
/// <param name="stream">The stream to read.</param>
public override void Load(Stream stream)
{
try
{
Data = JsonConfigurationFileParser.Parse(stream);
}
catch (JsonException e)
{
throw new FormatException(SR.Error_JSONParseError, e);
}
}
}

上面的操作_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged())); 就能解释的通了,原理是利用文件系统的GetReloadToken()令牌,

只是在FileConfigurationProvider 上封装了一层转换。

简单看下:FileConfigurationProvider,下面值保留了Load部分。

public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable
{
private void Load(bool reload)
{
IFileInfo file = Source.FileProvider?.GetFileInfo(Source.Path);
if (file == null || !file.Exists)
{
if (Source.Optional || reload) // Always optional on reload
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
else
{
var error = new StringBuilder($"The configuration file '{Source.Path}' was not found and is not optional.");
if (!string.IsNullOrEmpty(file?.PhysicalPath))
{
error.Append($" The physical path is '{file.PhysicalPath}'.");
}
HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString())));
}
}
else
{
// Always create new Data on reload to drop old keys
if (reload)
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} static Stream OpenRead(IFileInfo fileInfo)
{
if (fileInfo.PhysicalPath != null)
{
// The default physical file info assumes asynchronous IO which results in unnecessary overhead
// especally since the configuration system is synchronous. This uses the same settings
// and disables async IO.
return new FileStream(
fileInfo.PhysicalPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: 1,
FileOptions.SequentialScan);
} return fileInfo.CreateReadStream();
} using Stream stream = OpenRead(file);
try
{
Load(stream);
}
catch (Exception e)
{
HandleException(ExceptionDispatchInfo.Capture(e));
}
}
// REVIEW: Should we raise this in the base as well / instead?
OnReload();
} /// <summary>
/// Loads the contents of the file at <see cref="Path"/>.
/// </summary>
/// <exception cref="FileNotFoundException">If Optional is <c>false</c> on the source and a
/// file does not exist at specified Path.</exception>
public override void Load()
{
Load(reload: false);
}
}

看下上面的load,上面的load就是读取文件,然后交由JsonConfigurationProvider 的load调用 Data = JsonConfigurationFileParser.Parse(stream);转换为字典。

这就是为上文中的ConfigurationRoot 要调用一下load了。

上文的ConfigurationRoot 调用Load 部分。

foreach (IConfigurationProvider p in providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}

这就回到了对前面系列中的内存字典操作了,而FileConfigurationProvider 又继承ConfigurationProvider。

ConfigurationProvider 代码如下,主要是实现IConfigurationProvider接口,很多不同的文件配置都会用到这个,比如说ini文件、xml文件等等都会先转换为字典,然后继承ConfigurationProvider:

public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken(); /// <summary>
/// Initializes a new <see cref="IConfigurationProvider"/>
/// </summary>
protected ConfigurationProvider()
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} /// <summary>
/// The configuration key value pairs for this provider.
/// </summary>
protected IDictionary<string, string> Data { get; set; } /// <summary>
/// Attempts to find a value with the given key, returns true if one is found, false otherwise.
/// </summary>
/// <param name="key">The key to lookup.</param>
/// <param name="value">The value found at key if one is found.</param>
/// <returns>True if key has a value, false otherwise.</returns>
public virtual bool TryGet(string key, out string value)
=> Data.TryGetValue(key, out value); /// <summary>
/// Sets a value for a given key.
/// </summary>
/// <param name="key">The configuration key to set.</param>
/// <param name="value">The value to set.</param>
public virtual void Set(string key, string value)
=> Data[key] = value; /// <summary>
/// Loads (or reloads) the data for this provider.
/// </summary>
public virtual void Load()
{ } /// <summary>
/// Returns the list of keys that this provider has.
/// </summary>
/// <param name="earlierKeys">The earlier keys that other providers contain.</param>
/// <param name="parentPath">The path for the parent IConfiguration.</param>
/// <returns>The list of keys for this provider.</returns>
public virtual IEnumerable<string> GetChildKeys(
IEnumerable<string> earlierKeys,
string parentPath)
{
string prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter; return Data
.Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(kv => Segment(kv.Key, prefix.Length))
.Concat(earlierKeys)
.OrderBy(k => k, ConfigurationKeyComparer.Instance);
} private static string Segment(string key, int prefixLength)
{
int indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase);
return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength);
} /// <summary>
/// Returns a <see cref="IChangeToken"/> that can be used to listen when this provider is reloaded.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public IChangeToken GetReloadToken()
{
return _reloadToken;
} /// <summary>
/// Triggers the reload change token and creates a new one.
/// </summary>
protected void OnReload()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
previousToken.OnReload();
} /// <summary>
/// Generates a string representing this provider name and relevant details.
/// </summary>
/// <returns> The configuration name. </returns>
public override string ToString() => $"{GetType().Name}";
}

上述就是这个框架实现文件配置和文件监控的大致原理了。

这里再梳理一遍,使用JsonConfigurationFileParser.Parse 将steam流转换成字典,利用ChangeToken 对文件进行监听,如果有修改从加载即可。

好了,看完原理后,我们发现是ChangeToken的监听机制。那么问题来了,如果你看过上述ChangeToken的链接,你会发现RegisterChangeCallback只会调用一次。

原理很简单,因为这是令牌机制的,令牌过期了,那么这个RegisterChangeCallback自然调用一次,因为过期只有一次。

我们可以无限套娃方式:

static void Main(string[] args)
{
IConfigurationBuilder builder = new ConfigurationBuilder();
// builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.dev.json", optional: false, reloadOnChange: true);
builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.json",optional:false,reloadOnChange: true);
var configurationRoot = builder.Build(); Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]); Register(configurationRoot); Console.ReadKey();
} public static void Register(IConfigurationRoot configurationRoot)
{
IChangeToken token = configurationRoot.GetReloadToken(); token.RegisterChangeCallback(state =>
{
Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]);
Register(configurationRoot);
}, configurationRoot);
}

也可以这么做,利用ChangeToken 本身的方法:

static void Main(string[] args)
{
IConfigurationBuilder builder = new ConfigurationBuilder();
// builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.dev.json", optional: false, reloadOnChange: true);
builder.AddJsonFile(System.AppDomain.CurrentDomain.BaseDirectory + "/appsettings.json",optional:false,reloadOnChange: true);
var configurationRoot = builder.Build(); Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]); ChangeToken.OnChange(configurationRoot.GetReloadToken, () =>
{
Console.WriteLine(configurationRoot["key1"]);
Console.WriteLine(configurationRoot["key2"]);
}); Console.ReadKey();
}

这里OnChange的原理也是套娃,我把关键代码贴一下。

public static class ChangeToken
{
/// <summary>
/// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
/// </summary>
/// <param name="changeTokenProducer">Produces the change token.</param>
/// <param name="changeTokenConsumer">Action called when the token changes.</param>
/// <returns></returns>
public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
{
if (changeTokenProducer == null)
{
throw new ArgumentNullException(nameof(changeTokenProducer));
}
if (changeTokenConsumer == null)
{
throw new ArgumentNullException(nameof(changeTokenConsumer));
} return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
}
private class ChangeTokenRegistration<TState> : IDisposable
{
private void OnChangeTokenFired()
{
// The order here is important. We need to take the token and then apply our changes BEFORE
// registering. This prevents us from possible having two change updates to process concurrently.
//
// If the token changes after we take the token, then we'll process the update immediately upon
// registering the callback.
IChangeToken token = _changeTokenProducer(); try
{
_changeTokenConsumer(_state);
}
finally
{
// We always want to ensure the callback is registered
RegisterChangeTokenCallback(token);
}
} private void RegisterChangeTokenCallback(IChangeToken token)
{
IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>)s).OnChangeTokenFired(), this); SetDisposable(registraton);
}
}
}

同样是套娃工程,SetDisposable是关键,比我们自己写要好,回收机制利用到位,有兴趣可以看下。

以上只是个人整理,如有错误,望请指出,谢谢。

下一节配置系统之变色龙(环境配置)。

重新整理 .net core 实践篇—————配置系统之间谍[八](文件监控)的更多相关文章

  1. 重新整理 .net core 实践篇—————配置系统之军令状[七](配置文件)

    前言 介绍一下配置系统中的配置文件,很多服务的配置都写在配置文件中,也是配置系统的大头. 正文 在asp .net core 提供了下面几种配置文件格式的读取方式. Microsoft.extensi ...

  2. 重新整理 .net core 实践篇————配置系统之盟约[五]

    前言 在asp .net core 中我们会看到一个appsettings.json 文件,它就是我们在服务中的各种配置,是至关重要的一部门. 不管是官方自带的服务,还是我们自己编写的服务都是用它来实 ...

  3. 重新整理 .net core 实践篇—————配置系统之强类型配置[十]

    前言 前文中我们去获取value值的时候,都是通过configurationRoot 来获取的,如configurationRoot["key"],这种形式. 这种形式有一个不好的 ...

  4. 重新整理 .net core 实践篇————配置系统——军令(命令行)[六]

    前言 前文已经基本写了一下配置文件系统的一些基本原理.本文介绍一下命令行导入配置系统. 正文 要使用的话,引入Microsoft.extensions.Configuration.commandLin ...

  5. 重新整理 .net core 实践篇—————配置系统之简单配置中心[十一]

    前言 市面上已经有很多配置中心集成工具了,故此不会去实践某个框架. 下面链接是apollo 官网的教程,实在太详细了,本文介绍一下扩展数据源,和简单翻翻阅一下apollo 关键部分. apollo 服 ...

  6. 重新整理 .net core 实践篇————配置应用[一]

    前言 本来想整理到<<重新整理.net core 计1400篇>>里面去,但是后来一想,整理 .net core 实践篇 是偏于实践,故而分开. 因为是重新整理,那么就从配置开 ...

  7. 重新整理 .net core 实践篇—————日志系统之战地记者[十五]

    前言 本节开始整理日志相关的东西.先整理一下日志的基本原理. 正文 首先介绍一下包: Microsoft.Extengsion.Logging.Abstrations 这个是接口包. Microsof ...

  8. 重新整理 .net core 实践篇—————日志系统之作用域[十七]

    前言 前面介绍了服务与日志之间的配置,那么我们服务会遇到下面的场景会被遇到一些打log的问题. 前面我提及到我们的log,其实是在一个队列里面,而我们的请求是在并发的,多个用户同时发送请求这个时候我们 ...

  9. 重新整理 .net core 实践篇—————日志系统之结构化[十八]

    前言 什么是结构化呢? 结构化,就是将原本没有规律的东西进行有规律话. 就比如我们学习数据结构,需要学习排序然后又要学习查询,说白了这就是一套,没有排序,谈如何查询是没有意义的,因为查询算法就是根据某 ...

随机推荐

  1. 我与Git的那些破事(下)--分支模型

    在上篇文章中,我提到了Git的基本概念和一些本人实际项目中的总结.然而,最近读了Vincent Driessen写的一篇文章,觉得他总结的太好了,站在他肩膀上忍不住将自己的理解分享出来.Vincent ...

  2. matlab帮助文档

    matlab的纯文本帮助命令有多种,help.lookfor.which.doc.get.type等 help命令  help命令用来查询一个函数的使用方式. help fun %fun是函数名称   ...

  3. java语言写一个建议的五子棋

    经过16天的java学习,也学得了不少关于Java方面的知识,我想分享一下我用java写的一个简单的五子棋. 游戏规则: (1)对局双方各执一色棋子.(2)空棋盘开局.(3)白先.黑后,交替下子,每次 ...

  4. windows黑窗口命令笔记

    windows有个黑窗口,吃惊吧!意外吧!! 哈哈,我是真的有些吃惊的!! nslookup ipconfig /all ipconfig /flushdns windows 声音修复 windows ...

  5. hdu4067 费用流(混合欧拉的宽展和延伸)

    题意:        给以一个图,每个有向边都有两个权值,a,b其中a是保留这条边的花费,b是删除这条边的花费,让你删去一些边使图满足一下要求: (1)只有一个起点和一个终点 (2)所有的边都是又向的 ...

  6. hdu1255 扫描线,矩形重叠面积(两次以上)

    题意:       给你n个矩形,然后问你这n个矩形所组成的画面中被覆盖至少两次的面积有多大. 思路:       和1542差距并不是很大,大体上还是离散化+线段树扫面线,不同的地方就是这个题目要求 ...

  7. 推荐算法-聚类-K-MEANS

    对于大型的推荐系统,直接上协同过滤或者矩阵分解的话可能存在计算复杂度过高的问题,这个时候可以考虑用聚类做处理,其实聚类本身在机器学习中也常用,属于是非监督学习的应用,我们有的只是一组组数据,最终我们要 ...

  8. Redis数据结构—链表与字典

    目录 Redis数据结构-链表与字典 链表 Redis链表节点的结构 Redis链表的表示 Redis链表用在哪 字典 Redis字典结构总览 Redis字典结构分解 哈希算法 解决键冲突 rehas ...

  9. 谷歌浏览器安装Vue.js devtools

    第一步:访问谷歌商店 在之前的博客中已经谈到了这一点的实现方式 https://www.cnblogs.com/10134dz/p/13552777.html 第二步:下载Vue.js devtool ...

  10. 解决docker镜像无法删除的问题

    发现问题 来自守护进程的错误响应:冲突:无法删除050f26b6caca(必须强制) - 映像在多个存储库中被引用 Error response from daemon: conflict: unab ...