.Net Core自定义配置源从配置中心读取配置的方法
前言
配置,几乎所有的应用程序都离不开它。.Net Framework时代我们使用App.config、Web.config,到了.Net Core的时代我们使用appsettings.json,这些我们再熟悉不过了。然而到了容器化、微服务的时代,这些本地文件配置有的时候就不太合适了。当你把本地部署的服务搬到docker上后,你会发现要修改一个配置文件变的非常麻烦。你不得不通过宿主机进入容器内部来修改文件,也许容器内还不带vi等编辑工具,你连看都不能看,改都不能。更别说当你启动多个容器实例来做分布式应用的时候,一个个去修改容器的配置,这简直要命了。
因为这些原因,所以“配置中心”就诞生了。配置中心是微服务的基础设施,它对配置进行集中的管理并对外暴露接口,当应用程序需要的时候通过接口读取。配置通常为Key/Value模式,然后通过http接口暴露。好了,配置中心不多说了,感觉要偏了,这次是介绍怎么自定义一个配置源从配置中心读取配置。废话不多说直接上代码吧。
模拟配置中心
我们新建一个asp.net core webapi站点来模拟配置中心服务,端口配置到5000,并添加相应的controller来模拟配置中心对外的接口。
[Route("api/[controller]")] [ApiController] public class ConfigsController : ControllerBase { public List<KeyValuePair<string,string>> Get() { var configs = new List<KeyValuePair<string, string>>(); configs.Add(new KeyValuePair<string, string>("SecretKey","1238918290381923")); configs.Add(new KeyValuePair<string, string>("ConnectionString", "user=123;password=123;server=.")); return configs; } }
添加一个configscontroller,并修改Get方法,返回2个配置键值对。
访问下/api/configs看下返回是否正确
自定义配置源
从现在开始我们真正开始来定义一个自定义的配置源然后当程序启动的时候从配置中心读取配置文件信息,并提供给后面的代码使用配置。
新建一个asp.net core mvc站点来模拟客户端程序。
MyConfigProvider
public class MyConfigProvider : ConfigurationProvider { /// <summary> /// 尝试从远程配置中心读取配置信息 /// </summary> public async override void Load() { var response = ""; try { var serverAddress = "http://localhost:5000"; var client = new HttpClient(); client.BaseAddress = new Uri(serverAddress); response = await client.GetStringAsync("/api/configs"); } catch (Exception ex) { //write err log } if (string.IsNullOrEmpty(response)) { throw new Exception("Can not request configs from remote config center ."); } var configs = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(response); Data = new ConcurrentDictionary<string, string>(); configs.ForEach(c => { Data.Add(c); }); } }
新建一个MyConfigProvider的类,这个类从ConfigurationProvider继承,并重写其中的Load方法。使用HttpClient从配置中心读取信息后,进行反序列化,并把配置转换为字典。这里注意一下,虽然Data的类型为IDictionary<string,string>,但是这里实例化对象的时候使用了ConcurrentDictionary<string, string>类,因为Dictionary<string,string>是非线程安全的,如果进行多线程读写会出问题。
MyConfigSource
public class MyConfigSource : IConfigurationSource { public IConfigurationProvider Build(IConfigurationBuilder builder) { return new MyConfigProvider(); } }
新建一个MyConfigSource的类,这个类实现IConfigurationSource接口,IConfigurationSource接口只有一个Build方法,返回值为IConfigurationProvider,我们刚才定义的MyConfigProvider因为继承自ConfigurationProvider所以已经实现了IConfigurationProvider,我们直接new一个MyConfigProvider并返回。
MyConfigBuilderExt
public static class MyConfigBuilderExt { public static IConfigurationBuilder AddMyConfig( this IConfigurationBuilder builder ) { return builder.Add(new MyConfigSource()); } }
给IConfigurationBuilder定义一个AddMyConfig的扩展方法,跟.Net Core自带的几个配置源使用风格保持一致。当调用AddMyConfig的时候给IConfigurationBuilder实例添加一个MyConfigSource的源。
使用配置源
在Program中添加MyConfigSource
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, configBuiler) => { configBuiler.AddMyConfig(); }) .UseStartup<Startup>(); }
在ConfigureAppConfiguration的匿名委托方法中调用AddMyConfig扩展方法,这样程序启动的时候会自动使用MyConfigSource源并从配置中心读取配置到本地应用程序。
修改HomeController
public class HomeController : Controller { IConfiguration _configuration; public HomeController(IConfiguration configuration) { _configuration = configuration; } public IActionResult Index() { var secretKey = _configuration["SecretKey"]; var connectionString = _configuration["ConnectionString"]; ViewBag.SecretKey = secretKey; ViewBag.ConnectionString = connectionString; return View(); } }
修改homecontroller,把IConfiguration通过构造函数注入进去,在Index Action方法中读取配置,并赋值给ViewBag
修改Index视图
@{ ViewData["Title"] = "Test my config"; } <h3> SecretKey: @ViewBag.SecretKey </h3> <h3> ConnectionString: @ViewBag.ConnectionString </h3>
修改Index视图的代码,把配置信息从ViewBag中读取出来并在网页上展示。
运行一下
先运行配置中心站点再运行一下网站,首页出现了我们在配置中心定义的SecretKey跟ConnectionString信息,表示我们的程序成功的从配置中心读取了配置信息。我们的自定义配置源已经能够成功运行了。
改进
以上配置源虽然能够成功运行,但是仔细看的话显然它有2个比较大的问题。
- 配置中心的服务地址是写死在类里的。我们的配置中心很有可能会修改ip或者域名,写死在代码里显然不是高明之举,所以我们还是需要保留本地配置文件,把配置中心的服务地址写到本地配置文件中。
- 配置中心作为微服务的基础设施一旦故障会引发非常严重的后果,新启动或者重启的客户端会无法正常启动。如果我们在配置中心正常的时候冗余一份配置在本地,当配置中心故障的时候从本地读取配置,至少可以保证一部分客户端程序能够正常运行。
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "myconfigServer": "http://localhost:5000" }
修改本地appsettings.json文件,添加myconfigServer的配置信息。
public class MyConfigProvider : ConfigurationProvider { private string _serverAddress; public MyConfigProvider() { var jsonConfig = new JsonConfigurationSource(); jsonConfig.FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()); jsonConfig.Path = "appsettings.json"; var jsonProvider = new JsonConfigurationProvider(jsonConfig); jsonProvider.Load(); jsonProvider.TryGet("myconfigServer", out string serverAddress); if (string.IsNullOrEmpty(serverAddress)) { throw new Exception("Can not find myconfigServer's address from appsettings.json"); } _serverAddress = serverAddress; } /// <summary> /// 尝试从远程配置中心读取配置信息,当成功从配置中心读取信息的时候把配置写到本地的myconfig.json文件中,当配置中心无法访问的时候尝试从本地文件恢复配置。 /// </summary> public async override void Load() { var response = ""; try { var client = new HttpClient(); client.BaseAddress = new Uri(_serverAddress); response = await client.GetStringAsync("/api/configs"); WriteToLocal(response); } catch (Exception ex) { //write err log response = ReadFromLocal(); } if (string.IsNullOrEmpty(response)) { throw new Exception("Can not request configs from remote config center ."); } var configs = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(response); Data = new ConcurrentDictionary<string, string>(); configs.ForEach(c => { Data.Add(c); }); } private void WriteToLocal(string resp) { var file = Directory.GetCurrentDirectory() + "/myconfig.json"; File.WriteAllText(file,resp); } private string ReadFromLocal() { var file = Directory.GetCurrentDirectory() + "/myconfig.json"; return File.ReadAllText(file); } }
修改MyConfigProvider,修改构造函数,通过JsonConfigurationProvider从本地读取appsettings.json中的myconfigServer配置信息。新增WriteToLocal方法把配置中心返回的json数据写到本地文件中。新增ReadFromLocal方法,从本地文件读取json信息。
再次运行
先运行配置中心站点,再运行客户端网站,可以看到配置信息展示到首页界面上。关闭配置中心客跟客户端网站,并且重启客户端网站依然能够展示配置信息,说明自定义配置源当配置中心故障的时候成功从本地文件恢复了配置。图跟上面的图是一致的,就不贴了。
总结
通过以上我们定义了一个比较简单的自定义配置源,它能够通过http从配置中心读取配置,并且提供了同传统json配置文件一致的使用风格,最大程度的复用旧代码,减少因为引入配置中心而大规模改动代码。我们从上面的代码可以更清楚的知道.Net Core的配置源是如何工作的。ConfigurationSource只是ConfigurationProvider的建造器。真正完成配置加载、查找工作的是ConfigurationProvider。
以上代码还是演示级别的代码,还有很多改进的空间,比如http访问失败的重试,我们可以使用polly重构;比如支持定时从配置中心刷新配置等,有兴趣可以自己去实践一下。