zoukankan      html  css  js  c++  java
  • 源码解析.Net中IConfiguration配置的实现

    前言

    关于IConfituration的使用,我觉得大部分人都已经比较熟悉了,如果不熟悉的可以看这里。因为本篇不准备讲IConfiguration都是怎么使用的,但是在源码部分的解读,网上资源相对少一点,所以本篇准备着重源码这一块的设计,尽量的让读者能够理解它的内部实现。

    IConfiguration类之间的关系

    这里我整理了一个UML(可能不是那么标准,一些依赖关系没有体现)。可能直接看会有点不懂,下面我会慢慢讲这些东西。

    源码解析

    我们知道.net中的配置加载是有优先级的,如果有相同的key的话,一般后面加载的会覆盖前面的值,它们的优先级顺序如下图:

    • 在Host.CreateDefaultBuilder(args)执行的代码中,将委托添加到一个IConfigurationBuilder的集合中,builder为HostBuilder,hostingContext为HostBuilderContext,config就是IConfigurationBuilder,我们先来看下加载的代码,如下:
    builder.ConfigureAppConfiguration((hostingContext, config) =>
    {
        IHostEnvironment env = hostingContext.HostingEnvironment;
        bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
        config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange) //加载appsettings.json
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange); //根据环境变量加载appsettings.json
        if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
        {
            var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
            if (appAssembly is not null)
            {
                config.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange); //开发环境加载UserSecrets
            }
        }
        config.AddEnvironmentVariables(); //加载环境变量
        if (args is { Length: > 0 })
        {
            config.AddCommandLine(args); //加载命令行参数
        }
    })
    
    • 接下来我们拿其中一个例子,加载命令行参数,看看在AddCommandLine中都做了什么事情,是如何构建出IConfiguration对象的。
    1. 从UML图中可以看到,扩展对象都实现了IConfigurationSource,并且继承了抽象类ConfigurationProvider,源码如下:
    //ConfigurationBuilder扩展类
    public static class CommandLineConfigurationExtensions
    {
        public static IConfigurationBuilder AddCommandLine(this IConfigurationBuilder configurationBuilder, string[] args)
        {
            return configurationBuilder.AddCommandLine(args, switchMappings: null);
        }
        public static IConfigurationBuilder AddCommandLine(
            this IConfigurationBuilder configurationBuilder,
            string[] args,
            IDictionary<string, string> switchMappings)
        {
            //SwitchMappings是Key映射,可以看微软文档
            configurationBuilder.Add(new CommandLineConfigurationSource { Args = args, SwitchMappings = switchMappings });
            return configurationBuilder;
        }
    }
    public class CommandLineConfigurationSource : IConfigurationSource
    {
        public IDictionary<string, string> SwitchMappings { get; set; }
        public IEnumerable<string> Args { get; set; }
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new CommandLineConfigurationProvider(Args, SwitchMappings);
        }
    }
    //主要实现其Load方法,将kv加载到Data中,以便拿取。
    public class CommandLineConfigurationProvider : ConfigurationProvider
    {
        private readonly Dictionary<string, string> _switchMappings;
        public CommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null)
        {
            Args = args ?? throw new ArgumentNullException(nameof(args));
            //默认情况下mapping是null
            if (switchMappings != null)
            {
                //是个私有方法,源码就不贴了,可以自己下载来看看。
                _switchMappings = GetValidatedSwitchMappingsCopy(switchMappings);
            }
        }
        protected IEnumerable<string> Args { get; private set; }
        //load方法,主要是来设置kv,因为data是个字典类型,要把kv给设置和获取到。
        public override void Load()
        {
            var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            string key, value;
    
            using (IEnumerator<string> enumerator = Args.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    string currentArg = enumerator.Current;
                    int keyStartIndex = 0;
    
                    if (currentArg.StartsWith("--"))
                    {
                        keyStartIndex = 2;
                    }
                    else if (currentArg.StartsWith("-"))
                    {
                        keyStartIndex = 1;
                    }
                    else if (currentArg.StartsWith("/"))
                    {
                        currentArg = $"--{currentArg.Substring(1)}";
                        keyStartIndex = 2;
                    }
                    int separator = currentArg.IndexOf('=');
                    if (separator < 0)
                    {
                        if (keyStartIndex == 0)
                        {
                            continue;
                        }
                        if (_switchMappings != null && _switchMappings.TryGetValue(currentArg, out string mappedKey))
                        {
                            key = mappedKey;
                        }
                        else if (keyStartIndex == 1)
                        {
                            continue;
                        }
                        else
                        {
                            key = currentArg.Substring(keyStartIndex);
                        }
    
                        string previousKey = enumerator.Current;
                        if (!enumerator.MoveNext())
                        {
                            continue;
                        }
                        value = enumerator.Current;
                    }
                    else
                    {
                        string keySegment = currentArg.Substring(0, separator);
                        if (_switchMappings != null && _switchMappings.TryGetValue(keySegment, out string mappedKeySegment))
                        {
                            key = mappedKeySegment;
                        }
                        else if (keyStartIndex == 1)
                        {
                            throw new FormatException(SR.Format(SR.Error_ShortSwitchNotDefined, currentArg));
                        }
                        else
                        {
                            key = currentArg.Substring(keyStartIndex, separator - keyStartIndex);
                        }
    
                        value = currentArg.Substring(separator + 1);
                    }
    
                    data[key] = value;
                }
            }
            Data = data;
        }
    }
    

    2.在构建主机信息HostBuilder,下面只贴出主要代码,可以看到最后执行了ConfigurationBuilder的Build方法,构建IConfiguration,源码如下:

    public class HostBuilder : IHostBuilder
    {
        private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
        public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
        {
            _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
            return this;
        }
        //即我们在main函数中看到的Build方法
        public IHost Build()
        {
            //只能执行一次这个方法
            if (_hostBuilt)
            {
                throw new InvalidOperationException(SR.BuildCalled);
            }
            _hostBuilt = true;
            using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
            const string hostBuildingEventName = "HostBuilding";
            const string hostBuiltEventName = "HostBuilt";
    
            if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
            {
                Write(diagnosticListener, hostBuildingEventName, this);
            }
            //执行Host配置(应用程序执行路径,增加_dotnet环境变量,获取命令行参数,加载预配置)
            BuildHostConfiguration();
            //设置主机环境变量
            CreateHostingEnvironment();
            //设置上下文
            CreateHostBuilderContext();
            //构建程序配置(加载appsetting.json)
            BuildAppConfiguration();
            //构造容器,加载服务
            CreateServiceProvider();
            var host = _appServices.GetRequiredService<IHost>();
            if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
            {
                Write(diagnosticListener, hostBuiltEventName, host);
            }
    
            return host;
        }
        private void BuildAppConfiguration()
        {
            //对于已经加载过的配置不再重新加载
            IConfigurationBuilder configBuilder = new ConfigurationBuilder()
                .SetBasePath(_hostingEnvironment.ContentRootPath)
                .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);
    
            //注意这里是AppConfig
            foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
            {
                buildAction(_hostBuilderContext, configBuilder);
            }
            _appConfiguration = configBuilder.Build();
            //将新的配置赋值给config
            _hostBuilderContext.Configuration = _appConfiguration;
        }
    }
    
    1. ConfigurationBuilder的Build方法中加载对用Provider类里面的Load方法,加载配置信息。在获取对应Key值时,由加载的顺序,按照从后到前的顺序,依次查询key对应的value值,源码如下:
    public class ConfigurationBuilder : IConfigurationBuilder
    {
        //所有的配置源集合
        public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
        public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
        public IConfigurationBuilder Add(IConfigurationSource source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
    
            Sources.Add(source);
            return this;
        }
    
        public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (IConfigurationSource source in Sources)
            {
                //通过配置源构建provider对象。
                IConfigurationProvider provider = source.Build(this);
                providers.Add(provider);
            }
            //构建配置根对象
            return new ConfigurationRoot(providers);
        }
    }
    
     public class ConfigurationRoot : IConfigurationRoot, IDisposable
     {
        private readonly IList<IConfigurationProvider> _providers;
        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()));
            }
        }
        public IEnumerable<IConfigurationProvider> Providers => _providers;
        //通过索引拿数据,比如其他获取value的方法,其都是扩展类中实现的,读者可以自己看下。
        public string this[string key]
        {
            get
            {
                //从后往前查Porvider里面对应的KV,每个Provider里面都有一个Data(字典)对象,只获取第一个的。
                for (int i = _providers.Count - 1; i >= 0; i--)
                {
                    IConfigurationProvider provider = _providers[i];
    
                    if (provider.TryGet(key, out string value))
                    {
                        return value;
                    }
                }
                return null;
            }
            set
            {
                if (_providers.Count == 0)
                {
                    throw new InvalidOperationException(SR.Error_NoSources);
                }
                //修改时,所有的key对应的value值都修改
                foreach (IConfigurationProvider provider in _providers)
                {
                    provider.Set(key, value);
                }
            }
        }
    }
    

    总结

    1. 可以看到微软为了保留扩展性,增加了抽象类和抽象接口,让用户能够自定义的扩展实现自己的配置。

    2. 配置的加载顺序尤为重要,后加载的会覆盖前面的key。

    3. 文件监听,由于篇幅原因,不写那么多了,原理是通过ChangeToken和FileProvider实现的。

      这里只是把核心代码给提出来,并不是全部代码,所以读者如果想看其他的,需要自己翻下源码。其实在读源码的过程中,一方面是为了探究实现原理,更大的收获是能够学习到人家巧妙的设计,能够用到自己的代码中。其实笔者的水平的也不是很高,如果有差错的地方,望读者能够提出来,以便我及时改正。

  • 相关阅读:
    使用python打印九九乘法表
    python导入库的几种方式
    节点的介数
    连通图、强连通图、弱连通图
    python的基本格式化输出
    pycharm中的py文件重命名
    设置pycharm,在python程序的开头添加时间和作者等信息
    使用pandas库对csv文件进行筛选和保存
    python获取Excel表中某一格的数据
    使用python向txt文件写入内容
  • 原文地址:https://www.cnblogs.com/snailZz/p/15138473.html
Copyright © 2011-2022 走看看