asp.net core配置文件加载过程的深入了解
前言
配置文件中程序运行中,担当着不可或缺的角色;通常情况下,使用 visual studio 进行创建项目过程中,项目配置文件会自动生成在项目根目录下,如 appsettings.json,或者是被大家广泛使用的 appsettings.{env.environmentname}.json;
配置文件
作为一个入口,可以让我们在不更新代码的情况,对程序进行干预和调整,那么对其加载过程的全面了解就显得非常必要。
何时加载了默认的配置文件
在 program.cs 文件中,查看以下代码
public class program { public static void main(string[] args) { createwebhostbuilder(args).build().run(); } public static iwebhostbuilder createwebhostbuilder(string[] args) => webhost.createdefaultbuilder(args) .usestartup<startup>(); }
webhost.createdefaultbuilder
位于程序集 microsoft.aspnetcore.dll
内,当程序执行 webhost.createdefaultbuilder(args)
的时候,在 createdefaultbuilder 方法内部加载了默认的配置文件
代码如下
public static iwebhostbuilder createdefaultbuilder(string[] args) { var builder = new webhostbuilder(); if (string.isnullorempty(builder.getsetting(webhostdefaults.contentrootkey))) { builder.usecontentroot(directory.getcurrentdirectory()); } if (args != null) { builder.useconfiguration(new configurationbuilder().addcommandline(args).build()); } 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(new assemblyname(env.applicationname)); if (appassembly != null) { config.addusersecrets(appassembly, optional: true); } } config.addenvironmentvariables(); if (args != null) { config.addcommandline(args); } }) .configurelogging((hostingcontext, logging) => { logging.addconfiguration(hostingcontext.configuration.getsection("logging")); logging.addconsole(); logging.adddebug(); logging.addeventsourcelogger(); }) .configureservices((hostingcontext, services) => { // fallback services.postconfigure<hostfilteringoptions>(options => { if (options.allowedhosts == null || options.allowedhosts.count == 0) { // "allowedhosts": "localhost;127.0.0.1;[::1]" var hosts = hostingcontext.configuration["allowedhosts"]?.split(new[] { ';' }, stringsplitoptions.removeemptyentries); // fall back to "*" to disable. options.allowedhosts = (hosts?.length > 0 ? hosts : new[] { "*" }); } }); // change notification services.addsingleton<ioptionschangetokensource<hostfilteringoptions>>( new configurationchangetokensource<hostfilteringoptions>(hostingcontext.configuration)); services.addtransient<istartupfilter, hostfilteringstartupfilter>(); }) .useiis() .useiisintegration() .usedefaultserviceprovider((context, options) => { options.validatescopes = context.hostingenvironment.isdevelopment(); }); return builder; }
可以看到,createdefaultbuilder 内部还是使用了 iconfigurationbuilder 的实现,且写死了默认配置文件的名字
public static iwebhostbuilder createdefaultbuilder(string[] args) { var builder = new webhostbuilder(); if (string.isnullorempty(builder.getsetting(webhostdefaults.contentrootkey))) { builder.usecontentroot(directory.getcurrentdirectory()); } if (args != null) { builder.useconfiguration(new configurationbuilder().addcommandline(args).build()); } 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(new assemblyname(env.applicationname)); if (appassembly != null) { config.addusersecrets(appassembly, optional: true); } } config.addenvironmentvariables(); if (args != null) { config.addcommandline(args); } }) .configurelogging((hostingcontext, logging) => { logging.addconfiguration(hostingcontext.configuration.getsection("logging")); logging.addconsole(); logging.adddebug(); logging.addeventsourcelogger(); }) .configureservices((hostingcontext, services) => { // fallback services.postconfigure<hostfilteringoptions>(options => { if (options.allowedhosts == null || options.allowedhosts.count == 0) { // "allowedhosts": "localhost;127.0.0.1;[::1]" var hosts = hostingcontext.configuration["allowedhosts"]?.split(new[] { ';' }, stringsplitoptions.removeemptyentries); // fall back to "*" to disable. options.allowedhosts = (hosts?.length > 0 ? hosts : new[] { "*" }); } }); // change notification services.addsingleton<ioptionschangetokensource<hostfilteringoptions>>( new configurationchangetokensource<hostfilteringoptions>(hostingcontext.configuration)); services.addtransient<istartupfilter, hostfilteringstartupfilter>(); }) .useiis() .useiisintegration() .usedefaultserviceprovider((context, options) => { options.validatescopes = context.hostingenvironment.isdevelopment(); }); return builder; }
由于以上代码,我们可以在应用程序根目录下使用 appsettings.json
和 appsettings.{env.environmentname}.json
这种形式的默认配置文件名称
并且,由于 main 方法默认对配置文件进行了 build 方法的调用操作
public static void main(string[] args) { createwebhostbuilder(args).build().run(); }
我们可以在 startup.cs 中使用注入的方式获得默认的配置文件对象 iconfigurationroot/iconfiguration,代码片段
public class startup { public startup(iconfiguration configuration) { configuration = configuration; }
这是为什么呢,因为在 执行 build 方法的时候,方法内部已经将默认配置文件对象加入了 servicecollection 中,代码片段
var services = new servicecollection(); services.addsingleton(_options); services.addsingleton<ihostingenvironment>(_hostingenvironment); services.addsingleton<extensions.hosting.ihostingenvironment>(_hostingenvironment); services.addsingleton(_context); var builder = new configurationbuilder() .setbasepath(_hostingenvironment.contentrootpath) .addconfiguration(_config); _configureappconfigurationbuilder?.invoke(_context, builder); var configuration = builder.build(); services.addsingleton<iconfiguration>(configuration); _context.configuration = configuration;
以上这段代码非常熟悉,因为在 startup.cs 文件中,我们也许会使用过 servicecollection 对象将业务系统的自定义对象加入服务上下文中,以方便后续接口注入使用。
addjsonfile 方法的使用
通常情况下,我们都会使用默认的配置文件进行开发,或者使用 appsettings.{env.environmentname}.json 的文件名称方式来区分 开发/测试/产品 环境,根据环境变量加载不同的配置文件;可是这样一来带来了另外一个管理上的问题,产品环境的配置参数和开发环境
是不同的,如果使用环境变量的方式控制配置文件的加载,则可能导致密码泄露等风险;诚然,可以手工在产品环境创建此文件,但是这样一来,发布流程将会变得非常繁琐,稍有错漏文件便会被覆盖。
我们推荐使用 addjsonfile 加载产品环境配置,代码如下
public startup(iconfiguration configuration, ihostingenvironment env) { configuration = addcustomizedjsonfile(env).build(); } public configurationbuilder addcustomizedjsonfile(ihostingenvironment env) { var build = new configurationbuilder(); build.setbasepath(env.contentrootpath).addjsonfile("appsettings.json", true, true); if (env.isproduction()) { build.addjsonfile(path.combine("/data/sites/config", "appsettings.json"), true, true); } return build; }
通过 addcustomizedjsonfile 方法去创建一个 configurationbuilder 对象,并覆盖系统默认的 configurationbuilder 对象,在方法内部,默认加载开发环境的配置文件,在产品模式下,额外加载目录 /data/sites/config/appsettings.json 文件,
不同担心配置文件冲突问题,相同键值的内容将由后加入的配置文件所覆盖。
配置文件的变动
在调用 addjsonfile 时,我们看到该方法共有 5 个重载的方法
其中一个方法包含了 4 个参数,代码如下
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(); }); }
在此方法中,有一个参数 bool reloadonchange,从参数描述可知,该值指示在文件变动的时候是否重新加载,默认值为:false;一般在手动加载配置文件,即调用 addjsonfile 方法时,建议将该参数值设置为 true。
那么 .netcore 是如果通过该参数 reloadonchange 是来监控文件变动,以及何时进行重新加载的操作呢,看下面代码
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); }
在我们执行 .build 方法的时候,方法内部最后一行代码给我们利用 addjsonfile 方法的参数创建并返回了一个 configurationroot 对象
在 configurationroot 的构造方法中
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()); } }
我们看到,方法内部一次读取了通过 addjsonfile 方法加入的配置文件,并为每个配置文件单独分配了一个监听器 changetoken,并绑定当前文件读取对象 iconfigurationprovider.getreloadtoken
方法到监听器中
当文件产生变动的时候,监听器会收到一个通知,同时,对该文件执行原子操作
private void raisechanged() { var previoustoken = interlocked.exchange(ref _changetoken, new configurationreloadtoken()); previoustoken.onreload(); }
由于 addjsonfile 方法内部使用了 jsonconfigurationsource ,而 build 的重载方法构造了一个 jsonconfigurationprovider 读取对象,查看代码
public override iconfigurationprovider build(iconfigurationbuilder builder) { ensuredefaults(builder); return new jsonconfigurationprovider(this); }
在 jsonconfigurationprovider 继承自 fileconfigurationprovider 类,该类位于程序集 microsoft.extensions.configuration.json.dll
内
在 fileconfigurationprovider 的构造方法中实现了监听器重新加载配置文件的过程
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); }); } }
值得注意的是,该监听器不是在得到文件变动通知后第一时间去重新加载配置文件,方法内部可以看到,这里有一个 thread.sleep(source.reloaddelay)
,而 reloaddelay 的默认值为:250ms,该属性的描述为
- 获取或者设置重新加载将等待的毫秒数, 然后调用 "load" 方法。 这有助于避免在完全写入文件之前触发重新加载。默认值为250
- 让人欣慰的是,我们可以自定义该值,如果业务对文件变动需求不是特别迫切,您可以将该值设置为一个很大的时间,通常情况下,我们不建议那么做
结语
以上就是 asp.netcore 中配置文件加载的内部执行过程,从中我们认识到,默认配置文件是如何加载,并将默认配置文件如何注入到系统中的,还学习到了如果在不同的环境下,选择加载自定义配置文件的过程;但配置文件变动的时候,系统内部又是如何去把配置文件重新加载到内存中去的。