zoukankan      html  css  js  c++  java
  • .Net Core 中的选项Options

    .NetCore的配置选项建议结合在一起学习,不了解.NetCore 配置Configuration的同学可以看下我的上一篇文章 [.Net Core配置Configuration源码研究]

    由代码开始


    定义一个用户配置选项

    public class UserOptions
    {
        private string instanceId;
        private static int index = 0;
        public UserOptions()
        {
            instanceId = (++index).ToString("00");
            Console.WriteLine($"Create UserOptions Instance:{instanceId}");
        }
        public string Name { get; set; }
        public int Age { get; set; }
        public override string ToString() => $"Name:{Name} Age:{Age} Instance:{instanceId} ";
    }
    public class UserOptions2
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public override string ToString() => $" Name:{Name} Age:{Age}";
    }
    

    定义json配置文件:myconfig.json

    {
      "UserOption": {
        "Name": "ConfigName-zhangsan",
        "Age": 666
      }
    }
    

    创建ServiceCollection

    services = new ServiceCollection();
    var configBuilder = new ConfigurationBuilder().AddInMemoryCollection().AddJsonFile("myconfig.json", true, true);
    var iconfiguration = configBuilder.Build();
    services.AddSingleton<IConfiguration>(iconfiguration);
    

    示例代码

    services.Configure<UserOptions>(x => { x.Name = "张三"; x.Age = new Random().Next(1, 10000); });
    services.AddOptions<UserOptions2>().Configure<IConfiguration>((x, config) => { x.Name = config["UserOption:Name"]; x.Age = 100; }); ;
    services.PostConfigure<UserOptions>(x => { x.Name = x.Name + "Post"; x.Age = x.Age; });
    services.Configure<UserOptions>("default", x => { x.Name = "Default-张三"; x.Age = new Random().Next(1, 10000); });
    services.Configure<UserOptions>("config", configuration.GetSection("UserOption"));
    using (var provider = services.BuildServiceProvider())
    {
        using (var scope1 = provider.CreateScope())
        {
            PrintOptions(scope1, "Scope1");
        }
    
        //修改配置文件
        Console.WriteLine(string.Empty);
        Console.WriteLine("修改配置文件");
        var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "myconfig.json");
        File.WriteAllText(filePath, "{"UserOption": { "Name": "ConfigName-lisi", "Age": 777}}");
        //配置文件的change回调事件需要一定时间执行
        Thread.Sleep(300);
        Console.WriteLine(string.Empty);
    
        using (var scope2 = provider.CreateScope())
        {
            PrintOptions(scope2, "Scope2");
        }
    
        Console.WriteLine(string.Empty);
    
        using (var scope3 = provider.CreateScope())
        {
            PrintOptions(scope3, "Scope3");
        }
    }
    
    static void PrintOptions(IServiceScope scope, string scopeName)
    {
        var options1 = scope.ServiceProvider.GetService<IOptions<UserOptions>>();
        Console.WriteLine($"手动注入读取,IOptions,{scopeName}-----{ options1.Value}");
    
        var options2 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>();
        Console.WriteLine($"配置文件读取,IOptionsSnapshot,{scopeName}-----{ options2.Value}");
        var options3 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>();
        Console.WriteLine($"配置文件根据名称读取,IOptionsSnapshot,{scopeName}-----{ options3.Get("config")}");
    
        var options4 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>();
        Console.WriteLine($"配置文件读取,IOptionsMonitor,{scopeName}-----{ options4.CurrentValue}");
        var options5 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>();
        Console.WriteLine($"配置文件根据名称读取,IOptionsMonitor,{scopeName}-----{options5.Get("config")}");
    
        var options6 = scope.ServiceProvider.GetService<IOptions<UserOptions2>>();
        Console.WriteLine($"Options2-----{options6.Value}");
    }
    

    代码运行结果

    Create UserOptions Instance:01
    手动注入读取,IOptions,Scope1----- Name:张三Post Age:6575 Instance:01
    Create UserOptions Instance:02
    配置文件读取,IOptionsSnapshot,Scope1----- Name:张三Post Age:835 Instance:02
    Create UserOptions Instance:03
    配置文件根据名称读取,IOptionsSnapshot,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:03
    Create UserOptions Instance:04
    配置文件读取,IOptionsMonitor,Scope1----- Name:张三Post Age:1669 Instance:04
    Create UserOptions Instance:05
    配置文件根据名称读取,IOptionsMonitor,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:05
    Options2----- Name:ConfigName-zhangsan Age:100
    
    修改配置文件
    Create UserOptions Instance:06
    
    手动注入读取,IOptions,Scope2----- Name:张三Post Age:6575 Instance:01
    Create UserOptions Instance:07
    配置文件读取,IOptionsSnapshot,Scope2----- Name:张三Post Age:5460 Instance:07
    Create UserOptions Instance:08
    配置文件根据名称读取,IOptionsSnapshot,Scope2----- Name:ConfigName-lisi Age:777 Instance:08
    配置文件读取,IOptionsMonitor,Scope2----- Name:张三Post Age:1669 Instance:04
    配置文件根据名称读取,IOptionsMonitor,Scope2----- Name:ConfigName-lisi Age:777 Instance:06
    Options2----- Name:ConfigName-zhangsan Age:100
    
    手动注入读取,IOptions,Scope3----- Name:张三Post Age:6575 Instance:01
    Create UserOptions Instance:09
    配置文件读取,IOptionsSnapshot,Scope3----- Name:张三Post Age:5038 Instance:09
    Create UserOptions Instance:10
    配置文件根据名称读取,IOptionsSnapshot,Scope3----- Name:ConfigName-lisi Age:777 Instance:10
    配置文件读取,IOptionsMonitor,Scope3----- Name:张三Post Age:1669 Instance:04
    配置文件根据名称读取,IOptionsMonitor,Scope3----- Name:ConfigName-lisi Age:777 Instance:06
    Options2----- Name:ConfigName-zhangsan Age:100
    

    通过运行代码得到的结论

    • Options可通过手动初始化配置项配置(可在配置时读取依赖注入的对象)、或通过IConfiguration绑定配置
    • PostConfiger可在Configer基础上继续配置
    • 可通过IOptionsSnapshot或IOptionsMonitor根据配置名称读取配置项,未指定名称读取第一个注入的配置
    • IOptions和IOptionsMonitor生命周期为Singleton,IOptionsSnapshot生命周期为Scope
    • IOptionsMonitor可监听到配置文件变动去动态更新配置项

    问题

    • IOptions,IOptionsSnapshot,IOptionsMonitor 如何/何时注入、初始化
    • Options指定名称时内部是如何设置的
    • Options如何绑定的IConfiguration
    • IOptionsMonitor是如何同步配置文件变动的


    配合源码解决疑惑


    Configure注入

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
    {
        return services.Configure(Microsoft.Extensions.Options.Options.DefaultName, configureOptions);
    }
    
    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class
    {
    	services.AddOptions();
    	services.AddSingleton((IConfigureOptions<TOptions>)new ConfigureNamedOptions<TOptions>(name, configureOptions));
    	return services;
    }
    
    public static IServiceCollection AddOptions(this IServiceCollection services)
    {
    	services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
    	services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
    	services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
    	services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
    	services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
    	return services;
    }
    

    通过上面的源码可以发现,Options相关类是在AddOptions中注入的,具体的配置项在Configure中注入。

    如果不指定Configure的Name,也会有个默认的Name=Microsoft.Extensions.Options.Options.DefaultName

    那么我们具体的配置项存到哪里去了呢,在ConfigureNamedOptions这个类中,在Configer函数调用时,只是把相关的配置委托存了起来:

    public ConfigureNamedOptions(string name, Action<TOptions> action)
    {
    	Name = name;
    	Action = action;
    }
    

    OptionsManager

    private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache = new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);
    
    public TOptions Value => Get(Options.DefaultName);
    
    public virtual TOptions Get(string name)
    {
    	name = name ?? Options.DefaultName;
    	return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
    

    OptionsManager实现相对较简单,在查询时需要执行Name,如果为空就用默认的Name,如果缓存没有,就用Factory创建一个,否则就读缓存中的选项。

    IOptionsIOptionsSnapshot的实现类都是OptionsManager,只是生命周期不同。


    OptionsFactory

    那么OptionsFactory又是如何创建Options的呢?我们看一下他的构造函数,构造函数将所有ConfigurePostConfigure的初始化委托都通过构造函数保存在内部变量中

    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
    	{
            _setups = setups;
            _postConfigures = postConfigures;
    	}
    

    接下来看Create(有删改,与本次研究无关的代码没有贴出来):

    	public TOptions Create(string name)
    	{
            //首先创建对应Options的实例
    		TOptions val = Activator.CreateInstance<TOptions>();
            //循环所有的配置项,依次执行,如果对同一个Options配置了多次,最后一次的赋值生效
    		foreach (IConfigureOptions<TOptions> setup in _setups)
    		{
    			var configureNamedOptions = setup as IConfigureNamedOptions<TOptions>;
    			if (configureNamedOptions != null)
    			{
                    //Configure中会判断传入Name的值与本身的Name值是否相同,不同则不执行Action
                    //这解释了我们一开始的示例中,注入了三个UserOptions,但是在IOptionsSnapshot.Value中获取到的是第一个没有名字的
                    //因为Value会调用OptionsManager.Get(Options.DefaultName),进而调用Factory的Create(Options.DefaultName)
    				configureNamedOptions.Configure(name, val);
    			}
    			else if (name == Options.DefaultName)
    			{
    				setup.Configure(val);
    			}
    		}
            
            //PostConfigure没啥可多说了,名字判断逻辑与Configure一样
    		foreach (var postConfigure in _postConfigures)
    		{
    			postConfigure.PostConfigure(name, val);
    		}
    		
    		return val;
    	}
    

    NamedConfigureFromConfigurationOptions

    IConfiguration配置Options的方式略有不同

    对应Configure扩展方法最终调用的代码在Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions这个类中

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder) where TOptions : class
    {
    	services.AddOptions();
    	services.AddSingleton((IOptionsChangeTokenSource<TOptions>)new ConfigurationChangeTokenSource<TOptions>(name, config));
    	return services.AddSingleton((IConfigureOptions<TOptions>)new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
    }
    
    

    扩展方法里又注入了一个IOptionsChangeTokenSource,这个类的作用是提供一个配置文件变动监听的Token

    同时将IConfigureOptions实现类注册成了NamedConfigureFromConfigurationOptions

    NamedConfigureFromConfigurationOptions继承了ConfigureNamedOptions,在构造函数中用IConfiguration.Bind实现了生成Options的委托

    	public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
    		: base(name, (Action<TOptions>)delegate(TOptions options)
    		{
    			config.Bind(options, configureBinder);
    		})
    

    所以在Factory的Create函数中,会调用IConfigurationBind函数

    由于IOptionsSnapshot生命周期是Scope,在配置文件变动后新的Scope中会获取最新的Options


    ValidateOptions

    OptionsBuilder还包含了一个Validate函数,该函数要求传入一个Func<TOptions,bool>的委托,会注入一个单例的ValidateOptions对象。

    OptionsFactory构建Options的时候会验证Options的有效性,验证失败会抛出OptionsValidationException异常

    对于ValidateOptionsPostConfigureOptions都是构建Options实例时需要用到的主要模块,不过使用和内部实现都较为简单,应用场景也不是很多,本文就不对这两个类多做介绍了


    结论

    Configure扩展函数中会首先调用AddOptions函数

    IOptions,IOptionsSnapshot,IOptionsMonitor都是在AddOptions函数中注入的

    Configure配置的选项配置委托最终会保存到ConfigureNamedOptionsNamedConfigureFromConfigurationOptions

    IOptionsIOptionsSnapshot的实现类为OptionsManager

    OptionsManager通过OptionsFactory创建Options的实例,并会以Name作为键存到字典中缓存实例

    OptionsFactory会通过反射创建Options的实例,并调用ConfigureNamedOptions中的委托给实例赋值

    现在只剩下最后一个问题了,OptionsMonitor是如何动态更新选项的呢?

    其实前面的讲解中已经提到了一个关键的接口IOptionsChangeTokenSource,这个接口提供一个IChangeToken,通过ChangeToken监听这个Token就可以监听到文件的变动,我们来看下OptionsMonitor是否是这样做的吧!

    //构造函数
    public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
    {
        _factory = factory;
        _sources = sources;
        _cache = cache;
        //循环属于TOptions的所有IChangeToken
        foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
        {
            ChangeToken.OnChange(() => source.GetChangeToken(), delegate(string name)
                                 {
    
                                    //清除缓存 
                                    name = name ?? Options.DefaultName;
    								_cache.TryRemove(name);
                                 }, source.Name);
        }
    }
    
    
    
    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
    
    

    果然是这样的吧!

  • 相关阅读:
    SEO在网页制作中的应用
    日期控件选2016-01-01却变为2015-01-01问题
    IE8兼容placeholder的方案
    各种乱码,编码问题设置方法整理(UTF-8)
    JQuery简单实现图片轮播效果
    解决 jsp eclipse异常 【The import javax.servlet cannot be resolved】
    tomcat加载不了spring-webjar终极解决办法
    MyEclipse Web Project导入Eclipse Dynamic Web Project,无法部署到tomcat问 题
    2020-11-01助教一周总结(第九周)
    2020-10-25助教一周总结(第八周)
  • 原文地址:https://www.cnblogs.com/bluesummer/p/15236890.html
Copyright © 2011-2022 走看看