ASP.NET Core 2.2 : 二十三. 深入聊一聊配置的内部处理机制

上一章介绍了配置的多种数据源被注册、加载和获取的过程,本节看一下这个过程系统是如何实现的。(asp.net core 系列目录)

一、数据源的注册

在上一节介绍的数据源设置中,appsettings.json、命令行、环境变量三种方式是被系统自动加载的,这是因为系统在webhost.createdefaultbuilder(args)中已经为这三种数据源进了注册,那么就从这个方法说起。这个方法中同样调用了configureappconfiguration方法,代码如下:

public static iwebhostbuilder createdefaultbuilder(string[] args)
{
    var builder = newwebhostbuilder();
    //省略部分代码
    builder.usekestrel((buildercontext, options) =>
        {
            options.configure(buildercontext.configuration.getsection("kestrel"));
        })
        .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(newassemblyname(env.applicationname));
                if(appassembly != null)
                {
                    config.addusersecrets(appassembly, optional: true);
                }
            }

            config.addenvironmentvariables();
            if(args != null)
            {
                config.addcommandline(args);
            }
       })

       //省略部分代码

    return builder;
}

看一下其中的configureappconfiguration方法,加载的内容主要有四种,首先加载的是appsettings.json和appsettings.{env.environmentname}.json两个json文件,关于env.environmentname在前面的章节已经说过,常见的有development、staging 和 production三种值,在我们开发调试时一般是development,也就是会加载appsettings.json和appsettings. development.json两个json文件。第二种加载的是用户机密文件,这仅限于development状态下,会通过config.addusersecrets方法加载。第三种是通过config.addenvironmentvariables方法加载的环境变量,第四种是通过config.addcommandline方法加载的命令行参数。

注意:这里的configureappconfiguration方法这时候是不会被执行的,只是将这个方法作为一个action<webhostbuildercontext, iconfigurationbuilder> configuredelegate添加到了webhostbuilder的_configureservicesdelegates属性中。configureservicesdelegates是一个list<action<webhostbuildercontext, iconfigurationbuilder>>类型的集合。对应代码如下:

public iwebhostbuilder configureappconfiguration(action<webhostbuildercontext, iconfigurationbuilder> configuredelegate)
{
    if(configuredelegate == null)
    {
        throw new argumentnullexception(nameof(configuredelegate));
    }

    _configureappconfigurationbuilderdelegates.add(configuredelegate);
    returnthis;
}

上一节的例子中,我们在webhost.createdefaultbuilder(args)方法之后再次调用configureappconfiguration方法添加了一些自定义的数据源,这个方法也是没有执行,同样被添加到了这个集合中。直到webhostbuilder通过它的build()方法创建webhost的时候,才会遍历这个集合逐一执行。这段代码写在被build()方法调用的buildcommonservices()中:

private iservicecollection buildcommonservices(out aggregateexception hostingstartuperrors)
{
    //省略部分代码
    var builder = new configurationbuilder()
        .setbasepath(_hostingenvironment.contentrootpath)
        .addconfiguration(_config);

    foreach (var configureappconfiguration in _configureappconfigurationbuilderdelegates)
    {
        configureappconfiguration(_context, builder);
    }

    var configuration = builder.build();
    services.addsingleton<iconfiguration>(configuration);
    _context.configuration = configuration;
//省略部分代码
    return services;
}

首先创建了一个configurationbuilder对象,然后通过foreach循环逐一执行被添加到集合_configureappconfigurationbuilderdelegates中的configureappconfiguration方法,那么在执行的时候,这些不同的数据源是如何被加载的呢?这部分功能在namespace microsoft.extensions.configuration命名空间中。

以appsettings.json对应的config.addjsonfile("appsettings.json", optional: true, reloadonchange: true)方法为例,进一步看一下它的实现方式。首先介绍的是iconfigurationbuilder接口,对应的实现类是configurationbuilder,代码如下:

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;
        }
        //省略了iconfigurationroot build()方法,下文介绍
    }

configureappconfiguration方法中调用的addjsonfile方法来自jsonconfigurationextensions类,代码如下:

public static class jsonconfigurationextensions
{
//省略部分代码

    public static iconfigurationbuilder addjsonfile(this iconfigurationbuilder builder, ifileprovider provider, string path, bool optional, bool reloadonchange)
    {
        if (builder == null)
        {
            throw new argumentnullexception(nameof(builder));
        }
        if (string.isnullorempty(path))
        {
            throw new argumentexception(resources.error_invalidfilepath, nameof(path));
        }

        return builder.addjsonfile(s =>
        {
            s.fileprovider = provider;
            s.path = path;
            s.optional = optional;
            s.reloadonchange = reloadonchange;
            s.resolvefileprovider();
        });
    }
    public static iconfigurationbuilder addjsonfile(this iconfigurationbuilder builder, action<jsonconfigurationsource> configuresource)
        => builder.add(configuresource);
}

addjsonfile方法会创建一个jsonconfigurationsource并通过configurationbuilder的add(iconfigurationsource source)方法将这个jsonconfigurationsource添加到configurationbuilder的ilist<iconfigurationsource> sources集和中去。

同理,针对环境变量,存在对应的environmentvariablesextensions,会创建一个对应的environmentvariablesconfigurationsource添加到configurationbuilder的ilist<iconfigurationsource> sources集和中去。这样的还有commandlineconfigurationextensions和commandlineconfigurationsource等,最终结果就是会根据数据源的加载顺序,生成多个xxxconfigurationsource对象(它们都直接或间接实现了iconfigurationsource接口)添加到configurationbuilder的ilist<iconfigurationsource> sources集和中。

在program文件的webhost.createdefaultbuilder(args)方法中的configureappconfiguration方法被调用后,如果在createdefaultbuilder方法之后再次调用了configureappconfiguration方法并添加了数据源(如同上一节的例子),同样会生成相应的xxxconfigurationsource对象添加到configurationbuilder的ilist<iconfigurationsource> sources集和中。

注意:这里不是每一种数据源生成一个xxxconfigurationsource,而是按照每次添加生成一个xxxconfigurationsource,并且遵循添加的先后顺序。例如添加多个json文件,会生成多个jsonconfigurationsource。

这些configurationsource之间的关系如下图1:

 

图1

到这里各种数据源的收集工作完成,都添加到了configurationbuilder的ilist<iconfigurationsource> sources属性中。

回到buildcommonservices方法中,通过foreach循环逐一执行了configureappconfiguration方法获取到ilist<iconfigurationsource>之后,下一句是varconfiguration = builder.build(),这是调用configurationbuilder的build()方法创建了一个iconfigurationroot对象。对应代码如下:

public class configurationbuilder : iconfigurationbuilder
    {
        public ilist<iconfigurationsource> sources { get; } = new list<iconfigurationsource>();

        //省略部分代码

        public iconfigurationroot build()
        {
            var providers = new list<iconfigurationprovider>();
            foreach (var source in sources)
            {
                var provider = source.build(this);
                providers.add(provider);
            }
            return new configurationroot(providers);
        }

    }

这个方法主要体现了两个过程:首先,遍历ilist<iconfigurationsource> sources集合,主要调用其中的各个iconfigurationsource的build方法创建对应的iconfigurationprovider,最终生成一个list<iconfigurationprovider>;第二,通过集合list<iconfigurationprovider>创建了configurationroot。configurationroot实现了iconfigurationroot接口。

先看第一个过程,依然以jsonconfigurationsource为例,代码如下:

    public class jsonconfigurationsource : fileconfigurationsource
    {
        public override iconfigurationprovider build(iconfigurationbuilder builder)
        {
            ensuredefaults(builder);
            return new jsonconfigurationprovider(this);
        }
    }

jsonconfigurationsource会通过build方法创建一个名为jsonconfigurationprovider的对象。通过jsonconfigurationprovider的名字可知,它是针对json类型的,也就是意味着不同类型的iconfigurationsource创建的iconfigurationprovider类型也是不一样的,对应图18‑4中的iconfigurationsource,生成的iconfigurationprovider关系如下图2。

 

图2

系统中添加的多个数据源被转换成了一个个对应的configurationprovider,这些configurationprovider组成了一个configurationprovider的集合。

再看一下第二个过程,configurationbuilder的build方法的最后一句是return new configurationroot(providers),就是通过第一个过程创建的configurationprovider的集合创建configurationroot。configurationroot代码如下:

public class configurationroot : iconfigurationroot
    {
        private ilist<iconfigurationprovider> _providers;
        private configurationreloadtoken _changetoken = new configurationreloadtoken();

        public configurationroot(ilist<iconfigurationprovider> providers)
        {
            if (providers == null)
            {
                throw new argumentnullexception(nameof(providers));
            }

            _providers = providers;
            foreach (var p in providers)
            {
                p.load();
                changetoken.onchange(() => p.getreloadtoken(), () => raisechanged());
            }
        }
//省略部分代码
}

可以看出,configurationroot的构造方法主要的作用就是将configurationprovider的集合作为自己的一个属性的值,并遍历这个集合,逐一调用这些configurationprovider的load方法,并为changetoken的onchange方法绑定数据源的改变通知和处理方法。

二、数据源的加载

从图18‑5可知,所有类型数据源最终创建的xxxconfigurationprovider都继承自configurationprovider,所以它们都有一个load方法和一个idictionary<string, string> 类型的data 属性,它们是整个配置系统的重要核心。load方法用于数据源的数据的读取与处理,而data用于保存最终结果。通过逐一调用provider的load方法完成了整个配置系统的数据加载。

以jsonconfigurationprovider为例,它继承自fileconfigurationprovider,所以先看一下fileconfigurationprovider的代码:

public abstract class fileconfigurationprovider : configurationprovider
{
//省略部分代码
    private void load(bool reload)
    {
        var file = source.fileprovider?.getfileinfo(source.path);
        if (file == null || !file.exists)
        {
        //省略部分代码
        }
        else
        {
            if (reload)
            {
                data = new dictionary<string, string>(stringcomparer.ordinalignorecase);
            }
            using (var stream = file.createreadstream())
            {
                try
                {
                    load(stream);
                }
                catch (exception e)
                {
//省略部分代码
                }
            }
        }
        onreload();
    }
    public override void load()
    {
        load(reload: false);
}
    public abstract void load(stream stream);
} 

本段代码的主要功能就是读取文件生成stream,然后调用load(stream)方法解析文件内容。从图18‑5可知,jsonconfigurationprovider、iniconfigurationprovider、xmlconfigurationprovider都是继承自fileconfigurationprovider,而对应json、ini、xml三种数据源来说,只是文件内容的格式不同,所以将通用的读取文件内容的功能交给了fileconfigurationprovider来完成,而这三个子类的configurationprovider只需要将fileconfigurationprovider读取到的文件内容的解析即可。所以这个参数为stream 的load方法写在jsonconfigurationprovider、iniconfigurationprovider、xmlconfigurationprovider这样的子类中,用于专门处理自身对应的格式的文件。

jsonconfigurationprovider代码如下:

public class jsonconfigurationprovider : fileconfigurationprovider
{
    public jsonconfigurationprovider(jsonconfigurationsource source) : base(source) { }

    public override void load(stream stream)
    {
        try
        {
            data = jsonconfigurationfileparser.parse(stream);
        }
        catch (jsonreaderexception e)
        {
            string errorline = string.empty;
            if (stream.canseek)
            {
                stream.seek(0, seekorigin.begin);

                ienumerable<string> filecontent;
                using (var streamreader = new streamreader(stream))
                {
                    filecontent = readlines(streamreader);
                    errorline = retrieveerrorcontext(e, filecontent);
                }
            }

            throw new formatexception(resources.formaterror_jsonparseerror(e.linenumber, errorline), e);
        }
    }
   //省略部分代码
}

jsonconfigurationprovider中关于json文件的解析由jsonconfigurationfileparser.parse(stream)完成的。最终的解析结果被赋值给了父类configurationprovider的名为data的属性中。

所以最终每个数据源的内容都分别被解析成了idictionary<string, string>集合,这个集合作为对应的configurationprovider的一个属性。而众多configurationprovider组成的集合又作为configurationroot的属性。最终它们的关系图如下图3:

 

图3

到此,配置的加载与数据的转换工作完成。下图4展示了这个过程。

图4

三、配置的读取

第一节的例子中,通过_configuration["theme:color"]的方式获取到了对应的配置值,这是如何实现的呢?现在我们已经了解了数据源的加载过程,而这个_configuration就是数据源被加载后的最终产出物,即configurationroot,见图18‑7。它的代码如下:

public class configurationroot : iconfigurationroot
{
    private ilist<iconfigurationprovider> _providers;
    private configurationreloadtoken _changetoken = new configurationreloadtoken();

    //省略了上文已讲过的构造方法

    public ienumerable<iconfigurationprovider> providers => _providers;
    public string this[string key]
    {
        get
        {
            foreach (var provider in _providers.reverse())
            {
                string value;

                if (provider.tryget(key, out value))
                {
                    return value;
                }
            }

            return null;
        }

        set
        {
            if (!_providers.any())
            {
                throw new invalidoperationexception(resources.error_nosources);
            }

            foreach (var provider in _providers)
            {
                provider.set(key, value);
            }
        }
    }

    public ienumerable<iconfigurationsection> getchildren() => getchildrenimplementation(null);

    internal ienumerable<iconfigurationsection> getchildrenimplementation(string path)
    {
        return _providers
            .aggregate(enumerable.empty<string>(),
                (seed, source) => source.getchildkeys(seed, path))
            .distinct()
            .select(key => getsection(path == null ? key : configurationpath.combine(path, key)));
    }

    public ichangetoken getreloadtoken() => _changetoken;

    public iconfigurationsection getsection(string key) 
        => new configurationsection(this, key);

    public void reload()
    {
        foreach (var provider in _providers)
        {
            provider.load();
        }
        raisechanged();
    }

    private void raisechanged()
    {
        var previoustoken = interlocked.exchange(ref _changetoken, new configurationreloadtoken());
        previoustoken.onreload();
    }
}

对应_configuration["theme:color"]的读取方式的是索引器“string this[string key]”,通过查看其get方法可知,它是通过倒序遍历所有configurationprovider,在configurationprovider的data中尝试查找是否存在key为"theme:color"的值。这也说明了第一节的例子中,在theme.json中设置了theme对象的值后,原本在appsettings.json设置的theme的值被覆盖的原因。从图18‑6中可以看到,该值其实也是被读取并加载的,只是由于configurationroot的“倒序”遍历configurationprovider的方式导致后注册的theme.json中的theme值先被查找到了。同时验证了所有配置值均认为是string类型的约定。

configurationroot还有一个getsection方法,会返回一个iconfigurationsection对象,对应的是configurationsection类。它的代码如下:

public class configurationsection : iconfigurationsection
    {
        private readonly configurationroot _root;
        private readonly string _path;
        private string _key;

        public configurationsection(configurationroot root, string path)
        {
            if (root == null)
            {
                throw new argumentnullexception(nameof(root));
            }

            if (path == null)
            {
                throw new argumentnullexception(nameof(path));
            }

            _root = root;
            _path = path;
        }

        public string path => _path;
        public string key
        {
            get
            {
                if (_key == null)
                {
                    // key is calculated lazily as last portion of path
                    _key = configurationpath.getsectionkey(_path);
                }
                return _key;
            }
        }
        public string value
        {
            get
            {
                return _root[path];
            }
            set
            {
                _root[path] = value;
            }
        }
        public string this[string key]
        {
            get
            {
                return _root[configurationpath.combine(path, key)];
            }

            set
            {
                _root[configurationpath.combine(path, key)] = value;
            }
        }

        public iconfigurationsection getsection(string key) => _root.getsection(configurationpath.combine(path, key));

        public ienumerable<iconfigurationsection> getchildren() => _root.getchildrenimplementation(path);

        public ichangetoken getreloadtoken() => _root.getreloadtoken();
}

它的代码很简单,可以说没有什么实质的代码,它只是保存了当前路径和对configurationroot的引用。它的方法大多是通过调用configurationroot的对应方法完成的,通过它自身的路径计算在configurationroot中对应的key,从而获取对应的值。而configurationroot对配置值的读取功能以及数据源的重新加载功能(reload方法)也是通过configurationprovider实现的,实际数据也是保存在configurationprovider的data值中。所以configurationroot和configurationsection就像一个外壳,自身并不负责数据源的加载(或重载)与存储,只负责构建了一个配置值的读取功能。

而由于配置值的读取是按照数据源加载顺序的倒序进行的,所以对于key值相同的多个配置,只会读取后加载的数据源中的配置,那么configurationroot和configurationsection就模拟出了一个树状结构,如下图5:

 

图5

本图是以如下配置为例:

{
  "theme": {
    "name": "blue",
    "color": "#0921dc"
  }
}

configurationroot利用它制定的读取规则,将这样的配置模拟成了如图18‑8这样的树,它有这样的特性:

a.所有节点都认为是一个configurationsection,不同的是对于“theme”这样的节点的值为空(图中用空心椭圆表示),而“name”和“color”这样的节点有对应的值(图中用实心椭圆表示)。

b.由于对key值相同的多个配置只会读取后加载的数据源中的配置,所以不会出现相同路径的同名节点。例如第一节例子中多种数据源配置了“theme”值,在这里只会体现最后加载的配置项。

四、配置的更新

由于configurationroot未实际保存数据源中加载的配置值,所以配置的更新实际还是由对应的configurationprovider来完成。以jsonconfigurationprovider、iniconfigurationprovider、xmlconfigurationprovider为例,它们的数据源都是具体文件,所以对文件内容的改变的监控也是放在fileconfigurationprovider中。fileconfigurationprovider的构造方法中添加了对设置了对应文件的监控,当然这里会首先判断数据源的reloadonchange选项是否被设置为true了。

    public abstract class fileconfigurationprovider : configurationprovider
    {
        public fileconfigurationprovider(fileconfigurationsource source)
        {
            if (source == null)
            {
                throw new argumentnullexception(nameof(source));
            }
            source = source;

            if (source.reloadonchange && source.fileprovider != null)
            {
                changetoken.onchange(
                    () => source.fileprovider.watch(source.path),
                    () => {
                        thread.sleep(source.reloaddelay);
                        load(reload: true);
                    });
            }
        }
       //省略其他代码
}

所以当数据源发生改变并且reloadonchange被设置为true的时候,对应的configurationprovider就会重新加载数据。但configurationprovider更新数据源也不会改变它在configurationroot的ienumerable<iconfigurationprovider>列表中的顺序。如果在列表中存在a和b两个configurationprovider并且含有相同的配置项,b排在a后面,那么对于这些相同的配置项来说,a中的是被b中的“覆盖”的。即使a的数据更新了,它依然处于“被覆盖”的位置,应用中读取相应配置项的依然是读取b中的配置项。

五、配置的绑定

在第一节的例子中讲过了两种获取配置值的方式,类似这样_configuration["theme:name"]和_configuration.getvalue<string>("theme:color","#000000")可以获取到theme的name和color的值,那么就会有下面这样的疑问:

appsettings.json中存在如下这样的配置

{
  "theme": {
    "name": "blue",
    "color": "#0921dc"
  }
}

新建一个theme类如下:

    public class theme
    {
        public string name { get; set; }
        public string color { get; set; }
    }

是否可以将配置值获取并赋值到这样的一个theme的实例中呢?

当然可以,系统提供了这样的功能,可以采用如下代码实现:

     theme theme = new theme();
     _configuration.getsection("theme").bind(theme);

绑定功能由configurationbinder实现,逻辑不复杂,读者如果感兴趣的可自行查看其代码。

本文链接:https://2i3i.com/aspnetcore_23.html ,转载请注明来源地址。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇