@adens 8/28/2020 8:15:57 AM
配置,几乎所有的应用程序都离不开它。.Net Framework时代我们使用App.config、Web.config,到了.Net Core的时代我们使用appsettings.json,这些我们再熟悉不过了。然而到了容器化、微服务的时代,这些本地文件配置有的时候就不太合适了。当你把本地部署的服务搬到docker上后,你会发现要修改一个配置文件变的非常麻烦。你不得不通过宿主机进入容器内部来修改文件,也许容器内还不带vi等编辑工具,你连看都不能看,改都不能。更别说当你启动多个容器实例来做分布式应用的时候,一个个去修改容器的配置,这简直要命了。
因为这些原因,所以“配置中心”就诞生了。配置中心是微服务的基础设施,它对配置进行集中的管理并对外暴露接口,当应用程序需要的时候通过接口读取。配置通常为Key/Value模式,然后通过http接口暴露。
模拟提供一个方法获取配置
新建一个Web Api 项目,新建一个ConfigController,提供一个Get方法下载JSON
格式1的配置文件
[Route("[controller]")]
[ApiController]
public class ConfigController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
string config = "{\"ConnectString\":\"Server=(localdb)\\\\MSSQLLocalDB;Database=aspnet-config-7db2893b-375e-48bd-86a3-bb9779b72ebe;Trusted_Connection=True;MultipleActiveResultSets=true\"}";
return File( UTF8Encoding.UTF8.GetBytes(config), "application/json");
}
}
运行结果为:
2.1 新建一个测试的Web Api项目
2.2 新建一个RemoteConfigurationProvider类继承自ConfigurationProvider.
这个类用来连接配置中心的接口,获取JSON
格式的配置文件
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace Adens.Configuration.Test.Configuration
{
public class RemoteConfigurationProvider: ConfigurationProvider
{
public HttpClient Backchannel { get; set; }
public JsonConfigurationParser Parser { get; set; }
public override void Load()
{
Backchannel = new HttpClient();
Parser = new JsonConfigurationParser();
var requestMessage = new HttpRequestMessage(HttpMethod.Get,"https://localhost:4040/Config");
//Source.Events.SendingRequest(requestMessage);
try
{
var response = Backchannel.SendAsync(requestMessage)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
if (response.IsSuccessStatusCode)
{
using (var stream = response.Content.ReadAsStreamAsync()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult())
{
var data = Parser.Parse(stream, "");
Data = data;
}
}
}
catch (Exception)
{
throw;
}
}
}
}
2.3 从github上把JsonConfigurationParser方法Copy下来
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Adens.Configuration.Test.Configuration
{
public interface IConfigurationParser
{
/// <summary>
/// Parse the input stream into a configuration dictionary
/// </summary>
/// <param name="input">The stream to parse</param>
/// <param name="initialContext">The initial context prefix to add to all keys</param>
/// <returns></returns>
IDictionary<string, string> Parse(Stream input, string initialContext);
}
}
// JsonConfigurationParser.cs
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Adens.Configuration.Test.Configuration
{
/// <summary>
/// Taken from Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser
/// </summary>
public class JsonConfigurationParser : IConfigurationParser
{
private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _context = new Stack<string>();
private string _currentPath;
private JsonTextReader _reader;
/// <summary>
/// Parse the input stream into a configuration dictionary
/// </summary>
/// <param name="input">The stream to parse</param>
/// <param name="initialContext">The initial context prefix to add to all keys</param>
/// <returns></returns>
public IDictionary<string, string> Parse(Stream input, string initialContext)
{
try
{
_data.Clear();
_reader = new JsonTextReader(new StreamReader(input));
_reader.DateParseHandling = DateParseHandling.None;
var jsonConfig = JObject.Load(_reader);
if (!string.IsNullOrEmpty(initialContext)) { EnterContext(initialContext); }
VisitJObject(jsonConfig);
if (!string.IsNullOrEmpty(initialContext)) { ExitContext(); }
return _data;
}
catch (JsonReaderException e)
{
string errorLine = string.Empty;
if (input.CanSeek)
{
input.Seek(0, SeekOrigin.Begin);
IEnumerable<string> fileContent;
using (var streamReader = new StreamReader(input))
{
fileContent = ReadLines(streamReader);
errorLine = RetrieveErrorContext(e, fileContent);
}
}
throw new FormatException(string.Format(
"Could not parse the JSON file. Error on line number '{0}': '{1}'.",
e.LineNumber,
errorLine),
e);
}
}
private void VisitJObject(JObject jObject)
{
foreach (var property in jObject.Properties())
{
EnterContext(property.Name);
VisitProperty(property);
ExitContext();
}
}
private void VisitProperty(JProperty property)
{
VisitToken(property.Value);
}
private void VisitToken(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
VisitJObject(token.Value<JObject>());
break;
case JTokenType.Array:
VisitArray(token.Value<JArray>());
break;
case JTokenType.Integer:
case JTokenType.Float:
case JTokenType.String:
case JTokenType.Boolean:
case JTokenType.Bytes:
case JTokenType.Raw:
case JTokenType.Null:
VisitPrimitive(token);
break;
default:
throw new FormatException(string.Format(
"Unsupported JSON token '{0}' was found. Path '{1}', line {2} position {3}.",
_reader.TokenType,
_reader.Path,
_reader.LineNumber,
_reader.LinePosition));
}
}
private void VisitArray(JArray array)
{
for (int index = 0; index < array.Count; index++)
{
EnterContext(index.ToString());
VisitToken(array[index]);
ExitContext();
}
}
private void VisitPrimitive(JToken data)
{
var key = _currentPath;
if (_data.ContainsKey(key))
{
throw new FormatException(string.Format(
"A duplicate key '{0}' was found.",
key));
}
_data[key] = data.ToString();
}
private void EnterContext(string context)
{
_context.Push(context);
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}
private void ExitContext()
{
_context.Pop();
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}
private static IEnumerable<string> ReadLines(StreamReader streamReader)
{
string line;
do
{
line = streamReader.ReadLine();
yield return line;
} while (line != null);
}
private static string RetrieveErrorContext(JsonReaderException e, IEnumerable<string> fileContent)
{
string errorLine;
if (e.LineNumber >= 2)
{
var errorContext = fileContent.Skip(e.LineNumber - 2).Take(2).ToList();
errorLine = errorContext[0].Trim() + Environment.NewLine + errorContext[1].Trim();
}
else
{
var possibleLineContent = fileContent.Skip(e.LineNumber - 1).FirstOrDefault();
errorLine = possibleLineContent ?? string.Empty;
}
return errorLine;
}
}
}
2.4 新建一个RemoteConfigurationSource类.继承IConfigurationSource
Main方法里 CreateHostBuilder
Build时执行该类的Build方法添加一个配置源
public class RemoteConfigurationSource : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RemoteConfigurationProvider();
}
}
2.5 修改 Program 里面的代码
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, builder) => {
// 添加一个新的配置源
builder.Add(new RemoteConfigurationSource());
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
2.6 测试代码
public class HomeController : Controller
{
IConfiguration _configuration;
public HomeController(IConfiguration configuration)
{
_configuration = configuration;
}
public string Index()
{
return _configuration.GetValue<string>("ConnectString");
}
}
2.7 测试结果
Q:1.为什么使用JSON
格式的文件作为返回值,而不是KeyValue的键值对
A:为了适应以后在页面编辑配置,使用JSON
格式更加易读.多层嵌套数据和数组之类的数据可以更好的维护.而且和ASP.NET Core 使用的Appsettings.json的配置方式相同.
缺点就是需要专门的解析JSON
格式到KeyValue格式的方法.不过这个方法微软已经写在了Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser
这个方法里面.可以直接拿过来使用.同样由于已存在这个方法,可以随意改造接口返回KeyValue的键值对式的配置
Last Modification : 8/28/2020 8:15:57 AM