基于选项模式实现.NET Core的配置热更新
liebian365 2024-10-22 15:39 8 浏览 0 评论
作者 | 秦元培
出品 | CSDN(ID:CSDNnews)
头图 | CSDN 下载自东方 IC
最近在面试的时候,遇到了一个关于 .NET Core 配置热更新的问题,顾名思义,就是在应用程序的配置发生变化时,如何在不重启应用的情况下使用当前配置。从 .NET Framework 一路走来,对于 Web.Config 以及 App.Config 这两个配置文件,我们应该是非常熟悉了,通常情况下, IIS 会检测这两个配置文件的变化,并自动完成配置的加载,可以说它天然支持热更新,可当我们的视野伸向分布式环境的时候,这种配置方式就变得繁琐起来,因为你需要修改一个又一个配置文件,更不用说这些配置文件可能都是放在容器内部。而有经验的朋友,可能会想到,利用 Redis 的发布-订阅来实现配置的下发,这的确是一个非常好的思路。总而言之,我们希望应用可以随时感知配置的变化,所以,在今天这篇博客里,我们来一起聊聊 .NET Core 中配置热更新相关的话题,这里特指全新的选项模式(Options)。
Options三剑客
在 .NET Core 中,选项模式(Options)使用类来对一组配置信息进行强类型访问,因为按照接口分隔原则(ISP)和关注点分离这两个工程原则,应用的不同部件的配置应该是各自独立的,这意味着每一个用于访问配置信息的类,应该是只依赖它所需要的配置信息的。举一个简单的例子,虽然 Redis 和 MySQL 都属于数据持久化层的设施,但是两者属于不同类型的部件,它们拥有属于各自的配置信息,而这两套配置信息应该是相互独立的,即 MySQL 不会因为 Redis 的配置存在问题而停止工作。此时,选项模式(Options)推荐使用两个不同的类来访问各自的配置。我们从下面这个例子开始:
{
"Learning": {
"Years": 5,
"Topic": [ "Hotfix", ".NET Core", "Options" ],
"Skill": [
{
"Lang": "C#",
"Score": 3.9
},
{
"Lang": "Python",
"Score": 2.6
},
{
"Lang": "JavaScript",
"Score": 2.8
}
]
}
}
此时,如果希望访问 Learning节点下的信息,我们有很多种实现方式:
//方式1
var learningSection = Configuration.GetSection("Learning");
var careerYears = learningSection.GetValue<decimal>("Years");
var topicHotfix = learningSection.GetValue<string>("Topic:0");
//方式2
var careerYears = Configuration["Learning:Years"];
var topicHotfix = Configuration["Learning:Topic:0");
而更好的方式是,定义一个类来访问这组配置信息:
[Serializable]
public class LearningOptions
{
public decimal Years { get; set; }
public List<string> Topic { get; set; }
public List<SkillItem> Skill { get; set; }
}
[Serializable]
public class SkillItem
{
public string Lang { get; set; }
public decimal? Score { get; set; }
}
同样地,茴香的茴字有几种写法,你可知道?
//写法1:手动绑定
var leaningOptions = new LearningOptions;
Configuration.GetSection("Learning").Bind(leaningOptions);
//写法2:自动绑定
leaningOptions = Configuration.GetSection("Learning").Get<LearningOptions>;
//写法3:自动绑定 + 依赖注入
services.Configure<LearningOptions>(Configuration.GetSection("Learning"));
//写法4:配置的二次加工
services.PostConfigure<LearningOptions>(options => options.Years += 1);
//写法5:委托绑定
services.Configure<AppInfoOptions>(options =>
{
options.AppName = "ASP.NET Core";
options.AppVersion = "1.2.1";
});
我们知道,在 .NET Core 里依赖注入被提升到了一等公民的位置,可谓是无处不在。当我们在 IoC 容器中注入 LearningOptions以后,就可以在服务层或者控制器层直接使用它们,此时,我们就会遇到传说中的 Options 三剑客,即IOptions<TOptions>、IOptionsSnapshot<TOptions>和IOptionsMonitor<TOptions>。关于它们三个的区别,官方文档里给出了详细的说明:
IOptions:生命周期为 Singleton,在应用启动时完成初始化。应用启动后,对配置的修改是非响应式的。
IOptionsSnapshot:生命周期为 Scoped,每次请求时会重新计算选项。应用启动后,对配置的修改是响应式的。
IOptionsMonitor:生命周期为 Singleton,可以随时检索当前配置项。应用启动后,对配置的修改是响应式的。
是不是听起来有一点还有一点绕?长话短说就是,如果希望修改完配置立即生效,那么,更推荐使用 IOptionsSnapshot<TOptions> 和IOptionsMonitor<TOptions>,前者是在下一次请求时生效,后者则是访问CurrentValue的时候生效。而对于像3.14或者0.618这种运行时期间不会修改的“常量”,更推荐使用IOptions<TOptions>。下面是关于它们的一个例子:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IOptions<LearningOptions> _learningOptions;
private readonly IOptionsSnapshot<LearningOptions> _learningOptionsSnapshot;
private readonly IOptionsMonitor<LearningOptions> _learningOptionsMonitor;
private readonly IConfiguration _configuration;
public WeatherForecastController(ILogger<WeatherForecastController> logger,
IOptions<LearningOptions> learningOptions,
IOptionsSnapshot<LearningOptions> learningOptionsSnapshot,
IOptionsMonitor<LearningOptions> learningOptionsMonitor,
IConfiguration configuration
)
{
_logger = logger;
_learningOptions = learningOptions;
_learningOptionsSnapshot = learningOptionsSnapshot;
_learningOptionsMonitor = learningOptionsMonitor;
_configuration = configuration;
_learningOptionsMonitor.OnChange((options, value) =>
{
_logger.LogInformation($"OnChnage => {JsonConvert.SerializeObject(options)}");
});
}
[HttpGet("{action}")]
public ActionResult GetOptions
{
var builder = new StringBuilder;
builder.AppendLine("learningOptions:");
builder.AppendLine(JsonConvert.SerializeObject(_learningOptions.Value));
builder.AppendLine("learningOptionsSnapshot:");
builder.AppendLine(JsonConvert.SerializeObject(_learningOptionsSnapshot.Value));
builder.AppendLine("learningOptionsMonitor:");
builder.AppendLine(JsonConvert.SerializeObject(_learningOptionsMonitor.CurrentValue));
return Content(builder.ToString);
}
}
现在我们修改一下配置文件,因为我们为_learningOptionsMonitor注册了回调函数,可以在控制台看到对应的日志:
此时,我们通过 Postman 调用接口,我们会得到下面的结果:
可以注意到,此时,learningOptions 中的值依然是更新前的值,这就是它们三者的区别,清楚了吗?
除了这些以外,选项模式(Options)中还有一个需要注意的地方,是所谓的命名选项(IConfigureNamedOptions),主要用在多个 Section 绑定统一属性时。譬如现在的应用程序都流行深色主题,实际上深色主题和浅色主题具有相同的结构,比如前景色和背景色,两者唯一的区别是这些颜色配置不一样。考虑下面的配置信息:
{
"Themes": {
"Dark": {
"Foreground": "#fff",
"Background": "#000"
},
"White": {
"Foreground": "#000",
"Background": "#fff"
}
}
}
此时,我们该如何定义这个主题选项呢?
public class ThemeOptions
{
public string Foreground { get; set; }
public string Background { get; set; }
}
接下来,我们通过命名的方式来注入两个不同的主题:
services.Configure<ThemeOptions>("DarkTheme", Configuration.GetSection("Themes:Dark"));
services.Configure<ThemeOptions>("WhiteTheme", Configuration.GetSection("Themes:White"));
在任何你希望使用它们的地方,注入 IOptionsSnapshot<ThemeOptions> 和IOptionsMonitor<ThemeOptions> 即可,这两个类型都提供了一个Get 方法,传入前面定义好的主题就可以获取到对应的主题了。细心的朋友,应该会发现一件事情,这里三剑客只提到了后面两个,IOptions<ThemeOptions> 直接被无视了。请记住下面这段话:命名的选项只能通过 IOptionsSnapshot 和 IOptionsMonitor 来访问。所有选项都是命名实例。IConfigureOptions 实例将被视为面向 Options.DefaultName 实例,即 string.Empty。IConfigureNamedOptions 还可实现 IConfigureOptions。IOptionsFactory 的默认实现具有适当地使用每个实例的逻辑。 命名选项用于面向所有命名实例,而不是某一特定命名实例。ConfigureAll 和 PostConfigureAll 使用此约定。
IChnageToken
现在,让我们回到本文的主题,博主你不是要说配置热更新这个话题吗?截至到目前为止,我们修改配置文件的时候,ASP.NET Core 应用明明就会更新配置啊,所以,博主你到底想说什么?其实,博主想说的是,的确我们的目的已经达到了,但我们不能永远停留在“知其然”的水平,如果不试图去了解内在的机制,当我们去尝试实现一个自定义配置源的时候,就会遇到一些你没有办法想明白的事情。所以,接下来要讲的 IChnageToken 这个接口可以说是非常重要。
首先,我们把目光聚焦到 CreateDefaultBuilder这个方法,它通常在入口文件Program.cs 中被调用,主要作用是构造一个 IWebHostBuilder 实例并返回,下面是这个方法的内部实现,博主这里对其进行了精简:
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
//以下简化后的代码片段
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsDevelopment)
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != )
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables;
if (args != )
{
config.AddCommandLine(args);
}
})
}
可以注意到,通过 ConfigureAppConfiguration方法,框架主要做了下面的工作:
从 appsettings.json 和appsettings.${env.EnvironmentName}.json 两个配置文件中加载配置
从机密管理器中加载配载
从环境变量中加载配置
从命令行参数中加载配置
实际上,.NET Core 可以从配置文件、环境变量、Azure Key Vault、Azure 应用程序配置、命令行参数、已安装或已创建的自定义提供程序、目录文件、内存中的 .NET 对象等各种各样的来源中加载配置,这里的 appsettings.json 使用的是JsonConfigurationProvider 类,位于Microsoft.Extensions.Configuration.Json这个命名空间,可以注意到,它继承自FileConfigurationProvider类,并重写了Load 方法,通过这些关系,我们最终可以找到这样一段代码:
public FileConfigurationProvider(FileConfigurationSource source)
{
if (source == )
{
throw new ArgumentException(nameof(source));
}
Source = source;
if (Source.ReloadOnChange && Source.FileProvider != )
{
_changeTokenRegistration = ChangeToken.OnChange(
=> Source.FileProvider.Watch(Source.Path),
=> {
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
}
}
所以,真相就是,所有基于文件的配置提供者,都依赖于 FileConfigurationSource,而通过FileConfigurationSource 暴露出来的FileProvider 都具备监视文件变化的能力,更本质上的代码其实应该是下面这样:
//ChangeToken + IFileProvider 实现对文件的监听
var filePath = @"C:\Users\admin\Downloads\孔乙己.txt";
var directory = System.IO.Path.GetDirectoryName(filePath);
var fileProvider = new PhysicalFileProvider(directory);
ChangeToken.OnChange(
=> fileProvider.Watch("孔乙己.txt"),
=> {
_logger.LogInformation("孔乙己,你一定又偷人家书了吧!");
}
);
所以,真相只有一个,真正帮助我们实现配置热更新的,其实是 IChangeToken 这个接口,我们只需要把这样一个实例传入到ChangeToken.OnChange 方法中,就可以在特定的时机触发这个回调函数,而显然,对于大多数的IConfigurationProvider 接口而言,这个回调函数其实就是Load 方法,关于微软提供的ChangeToken 静态类的实现,大家如果有兴趣去了解的话,可以参考这里:https://github.com/dotnet/extensions/blob/release/3.1/src/Primitives/src/ChangeToken.cs。话说回来,我们说IOptionsSnapshot<T>和IOptionsMonitor<T>是响应式的,当配置发生改变的时候,它们对应的值会跟着改变,从某种意义上来说,是因为IChangeToken提供了这样一个可以监听变化的的能力,试想一下,我们只需要给每一个IConfigurationProvider对应的IChangeToken注册相同的回调函数,那么,当某一个IConfigurationProvider 需要重新加载的时候,我们就可以针对这个IConfigurationProvider里对应的键值对进行处理。事实上,微软官方在实现IConfigurationRoot 的时候,的确就是这样做的:
public class ConfigurationRoot : IConfigurationRoot
{
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken;
private IList<IConfigurationProvider> _providers;
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
_providers = providers;
foreach (var provider in providers)
{
provider.Load;
ChangeToken.OnChange( => provider.GetReloadToken, this.RaiseChanged);
}
}
public IChangeToken GetReloadToken => return _changeToken;
private void RaiseChanged
{
Interlocked.Exchange<ConfigurationReloadToken>(ref _changeToken, new ConfigurationReloadToken).OnReload;
}
public void Reload
{
foreach (var provider in _providers)
{
provider.Load;
}
this.RaiseChanged;
}
}
自定义配置源
好了,现在你可以说你了解 .NET Core 的配置热更新这个话题了,因为截至到此时此刻,我们不仅仅达到了一开始的目的,而且深刻地理解了它背后蕴含的原理。这样,我们就可以向着下一个目标:自定义配置源努力了。前面提到过,.NET Core 里面支持各种各样的配置源,实际中可能会遇到更多的配置源,比如不同的数据库、YAML 格式以及 Apollo、Consul、Nacos 这些配置中心等等,所以,了解如何去写一个自定义的配置源还是非常有必要的。我们在一开始的时候提到了 Redis 的发布-订阅,那么,下面我们就来基于发布-订阅实现一个简单的配置中心,当我们需要修改配置时,只需要通过可视化的 Redis 工具进行修改,然后再给指定的客户端发一条消息即可。
实现自定义配置源,需要实现 IConfigurationSource 和IConfigurationProvider 两个接口,前者实现起来非常简单,因为只要返回我们定义的RedisConfigurationProvider 实例即可:
public class RedisConfigurationSource : IConfigurationSource
{
private readonly RedisConfigurationOptions _options;
public RedisConfigurationSource(RedisConfigurationOptions options)
{
_options = options;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RedisConfigurationProvider(_options);
}
}
接下来是 RedisConfigurationProvider 类的实现:
public class RedisConfigurationProvider : ConfigurationProvider
{
private CSRedisClient _redisClient;
private readonly RedisConfigurationOptions _options;
public RedisConfigurationProvider(RedisConfigurationOptions options )
{
_options = options;
_redisClient = new CSRedisClient(_options.ConnectionString);
if (options.AutoReload)
{
//利用Redis的发布-订阅重新加载配置
_redisClient.Subscribe((_options.HashCacheChannel, msg => Load));
}
}
public override void Load
{
Data = _redisClient.HGetAll<string>(_options.HashCacheKey) ?? new Dictionary<string, string>;
}
}
为了用起来更得心应手,扩展方法是少不了的:
public class RedisConfigurationProvider : ConfigurationProvider
{
private CSRedisClient _redisClient;
private readonly RedisConfigurationOptions _options;
public RedisConfigurationProvider(RedisConfigurationOptions options )
{
_options = options;
_redisClient = new CSRedisClient(_options.ConnectionString);
if (options.AutoReload)
{
//利用Redis的发布-订阅重新加载配置
_redisClient.Subscribe((_options.HashCacheChannel, msg => Load));
}
}
public override void Load
{
Data = _redisClient.HGetAll<string>(_options.HashCacheKey) ?? new Dictionary<string, string>;
}
}
现在,我们改一下入口类 Program.cs,因为在这个阶段依赖注入是无法使用的,所以,看起来有一点难受,从命名就可以看出来,内部使用了Hash 这种结构,理论上每个客户端应该使用不同的 Key 来进行缓存,应该使用不同的 Channel 来接收配置更新的通知:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(configurationBuilder =>
{
configurationBuilder.AddRedisConfiguration(new Models.RedisConfigurationOptions
{
AutoReload = true,
ConnectionString = "127.0.0.1:6379",
HashCacheKey = "aspnet:config",
HashCacheChannel = "aspnet:config:change"
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>;
});
假设现在Redis里存储着下图所示的信息:
相应地,我们可以在Startup中进行绑定:
services.Configure<AppInfoOptions>(Configuration.GetSection("App"));
调一下接口看看?完全一致!Yes!
本文小结
回想起这个面试中“邂逅”的问题,针对对这块内容,其实当时并没有和面试官进行太深的交流,提到了分布式配置、配置中心以及像缓存的雪崩、击穿等等常见的问题,我隐约记得配置文件 appsettings.json配置的部分有热更新的配置项,但我并没有对选项模式(Options)里的三剑客做过深入的挖掘,所以,这篇博客,一方面是系统地了解了一下选项模式(Options)的使用,而另一方面是由配置热更新这个话题引申出来的一系列细节,在没有理解IChangeToken 的时候,实现一个自定义的配置源是有一点困难的,在这篇博客的最后,我们基于 Redis 的发布-订阅实现了一个简单的配置中心,不得不说,Redis里用:来分割 Key 的方式,实在是太棒了,因为它可以完美地和 .NET Core 里的配置系统整合起来,这一点只能用赏心悦目来形容。
声明:本文为 CSDN 博主 PayneQin 的原创文章,版权归作者所有。
原文地址:https://blog.csdn.net/qinyuanpei/article/details/109040253?spm=1000.2115.3001.4373
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)