• Asp.NetCore源码学习[1-2]:配置[Option]


    Asp.NetCore源码学习[1-2]:配置[Option]

    在上一篇文章中,我们知道了可以通过IConfiguration访问到注入的ConfigurationRoot,但是这样只能通过索引器IConfiguration["配置名"]访问配置。这篇文章将一下如何将IConfiguration映射到强类型。

    本系列源码地址

    一、使用强类型访问Configuration的用法

    指定需要配置的强类型MyOptions和对应的IConfiguration

    public void ConfigureServices(IServiceCollection services)
    {
        //使用Configuration配置Option
        services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));
        //载入Configuration后再次进行配置
        services.PostConfigure<MyOptions>(options=> { options.FilePath = "/"; });
    }
    

    在控制器中通过DI访问强类型配置,一共有三种方法可以访问到强类型配置MyOptions,分别是IOptionsIOptionsSnapshotIOptionsMonitor。先大概了解一下这三种方法的区别:

    public class ValuesController : ControllerBase
    {
        private readonly MyOptions _options1;
        private readonly MyOptions _options2;
        private readonly MyOptions _options3;
        private readonly IConfiguration _configurationRoot;
    
        public ValuesController(IConfiguration configurationRoot, IOptionsMonitor<MyOptions> options1, IOptionsSnapshot<MyOptions> options2, 
            IOptions<MyOptions> options3 )
        {
            //IConfiguration(ConfigurationRoot)随着配置文件进行更新(需要IConfigurationProvider监听配置源的更改)
            _configurationRoot = configurationRoot;
            //单例,监听IConfiguration的IChangeToken,在配置源发生改变时,自动删除缓存
            //生成新的Option实例并绑定,加入缓存
            _options1 = options1.CurrentValue;
            //scoped,每次请求重新生成Option实例并从IConfiguration获取数据进行绑定
            _options2 = options2.Value;
            //单例,从IConfiguration获取数据进行绑定,只绑定一次
            _options3 = options3.Value;
        }
    }
    

    二、源码解读

    首先看看Configure扩展方法,方法很简单,通过DI注入了Options需要的依赖。这里注入了了三种访问强类型配置的方法所需的所有依赖,接下来我们按照这三种方法去分析源码。

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
        => services.Configure<TOptions>(Options.Options.DefaultName, config, _ => { });
        
    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));
    }
    
    /// 为IConfigurationSection实例注册需要绑定的TOptions
    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;
    }
    

    1. 通过IOptions访问强类型配置

    与其有关的注入只有三个:

    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
    
    services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
    
    services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
    

    从以上代码我们知道,通过IOptions访问到的其实是OptionsManager实例。

    1.1 OptionsManager 的实现

    通过IOptionsFactory<>创建TOptions实例,并使用OptionsCache<>充当缓存。OptionsCache<>实际上是通过ConcurrentDictionary实现了IOptionsMonitorCache接口的缓存实现,相关代码没有展示。

    public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class
    {
        private readonly IOptionsFactory<TOptions> _factory;
    
        // 单例OptionsManager的私有缓存,通过ConcurrentDictionary实现了 IOptionsMonitorCache接口
        // Di中注入的单例OptionsCache<> 是给 OptionsMonitor<>使用的
        private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // Note: this is a private cache
    
        public OptionsManager(IOptionsFactory<TOptions> factory)
        {
            _factory = factory;
        }
    
        public TOptions Value
        {
            get
            {
                return Get(Options.DefaultName);
            }
        }
    
        public virtual TOptions Get(string name)
        {
            name = name ?? Options.DefaultName;
            return _cache.GetOrAdd(name, () => _factory.Create(name));
        }
    }
    

    1.2 IOptionsFactory 的实现

    首先通过Activator创建TOptions的实例,然后通过IConfigureNamedOptions.Configure()方法配置实例。该工厂类依赖于注入的一系列IConfigureOptions,在Di中注入的实现为NamedConfigureFromConfigurationOptions,其通过委托保存了配置源和绑定的方法

    /// Options工厂类 生命周期:Transient
    /// 单例OptionsManager和单例OptionsMonitor持有不同的工厂实例
    public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class
    {
        private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
        private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
    
        public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
        {
            _setups = setups;
            _postConfigures = postConfigures;
        }
    
        public TOptions Create(string name)
        {
            var options = CreateInstance(name);
            foreach (var setup in _setups)
            {
                if (setup is IConfigureNamedOptions<TOptions> namedSetup)
                {
                    namedSetup.Configure(name, options);
                }
                else if (name == Options.DefaultName)
                {
                    setup.Configure(options);
                }
            }
            foreach (var post in _postConfigures)
            {
                post.PostConfigure(name, options);
            }
    
            return options;
        }
    
        protected virtual TOptions CreateInstance(string name)
        {
            return Activator.CreateInstance<TOptions>();
        }
    }
    

    1.3 NamedConfigureFromConfigurationOptions 的实现

    在内部通过Action委托,保存了IConfiguration.Bind()方法。该方法实现了从IConfigurationTOptions实例的赋值。
    此处合并了NamedConfigureFromConfigurationOptionsConfigureNamedOptions的代码。

    public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
        where TOptions : class
    {
        public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
            : this(name, config, _ => { })
        { }
    
        public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
            : this(name, options => config.Bind(options, configureBinder))
        { }
        
        public ConfigureNamedOptions(string name, Action<TOptions> action)
        {
            Name = name;
            Action = action;
        }
    
        public string Name { get; }
    
        public Action<TOptions> Action { get; }
    
        public virtual void Configure(string name, TOptions options)
        {
            if (Name == null || name == Name)
            {
                Action?.Invoke(options);
            }
        }
    
        public void Configure(TOptions options) => Configure(string.Empty, options);
    }
    

    由于OptionsManager<>是单例模式,只会从IConfiguration中获取一次数据,在配置发生更改后,OptionsManager<>返回的TOptions实例不会更新。

    2. 通过IOptionsSnapshot访问强类型配置

    该方法和第一种相同,唯一不同的是,在注入DI系统的时候,其生命周期为scoped,每次请求重新创建OptionsManager<>。这样每次获取TOptions实例时,会新建实例并从IConfiguration重新获取数据对其赋值,那么TOptions实例的值自然就是最新的。

    3. 通过IOptionsMonitor访问强类型配置

    与其有关的注入有五个:

    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
    
    services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
    
    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
    
    services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
    
    services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
    

    第二种方法在每次请求时,都新建实例进行绑定,对性能会有影响。如何监测IConfiguration的变化,在变化的时候进行重新获取TOptions实例呢?答案是通过IChangeToken去监听配置源的改变。从上一篇知道,当使用FileProviders监听文件更改时,会返回一个IChangeToken,在FileProviders中监听返回的IChangeToken可以得知文件发生了更改并进行重新加载文件数据。所以使用IConfiguration 访问到的ConfigurationRoot 永远都是最新的。在IConfigurationProviderIConfigurationRoot中也维护了IChangeToken字段,这是用于向外部一层层的传递更改通知。下图为更改通知的传递方向:

    graph LR
    A["FileProviders"]--IChangeToken-->B
    B["IConfigurationProvider"]--IChangeToken-->C["IConfigurationRoot"]
    

    由于NamedConfigureFromConfigurationOptions没有直接保存IConfiguration字段,所以没办法通过它获取IConfiguration.GetReloadToken()。在源码中通过注入ConfigurationChangeTokenSource实现获取IChangeToken的目的

    3.1 ConfigurationChangeTokenSource的实现

    该类保存IConfiguration,并实现IOptionsChangeTokenSource接口

    public class ConfigurationChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>
    {
        private IConfiguration _config;
    
        public ConfigurationChangeTokenSource(IConfiguration config) : this(string.Empty, config)
        { }
    
        public ConfigurationChangeTokenSource(string name, IConfiguration config)
        {
            _config = config;
            Name = name ?? string.Empty;
        }
    
        public string Name { get; }
    
        public IChangeToken GetChangeToken()
        {
            return _config.GetReloadToken();
        }
    }
    

    3.2 OptionsMonitor的实现

    该类通过IOptionsChangeTokenSource获取IConfigurationIChangeToken。通过监听更改通知,在配置源发生改变时,删除缓存,重新绑定强类型配置,并加入到缓存中。IOptionsMonitor接口还有一个OnChange()方法,可以注册更改通知发生时候的回调方法,在TOptions实例发生更改的时候,进行回调。值得一提的是,该类有一个内部类ChangeTrackerDisposable,在注册回调方法时,返回该类型,在需要取消回调时,通过ChangeTrackerDisposable.Dispose()取消刚刚注册的方法。

        public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions>, IDisposable where TOptions : class
        {
            private readonly IOptionsMonitorCache<TOptions> _cache;
            private readonly IOptionsFactory<TOptions> _factory;
            private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
            private readonly List<IDisposable> _registrations = new List<IDisposable>();
            internal event Action<TOptions, string> _onChange;
    
            public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
            {
                _factory = factory;
                _sources = sources;
                _cache = cache;
    
                foreach (var source in _sources)
                {
                    var registration = ChangeToken.OnChange(
                          () => source.GetChangeToken(),
                          (name) => InvokeChanged(name),
                          source.Name);
    
                    _registrations.Add(registration);
                }
            }
    
            private void InvokeChanged(string name)
            {
                name = name ?? Options.DefaultName;
                _cache.TryRemove(name);
                var options = Get(name);
                if (_onChange != null)
                {
                    _onChange.Invoke(options, name);
                }
            }
    
            public TOptions CurrentValue
            {
                get => Get(Options.DefaultName);
            }
    
            public virtual TOptions Get(string name)
            {
                name = name ?? Options.DefaultName;
                return _cache.GetOrAdd(name, () => _factory.Create(name));
            }
    
            public IDisposable OnChange(Action<TOptions, string> listener)
            {
                var disposable = new ChangeTrackerDisposable(this, listener);
                _onChange += disposable.OnChange;
                return disposable;
            }
    
            public void Dispose()
            {
                foreach (var registration in _registrations)
                {
                    registration.Dispose();
                }
    
                _registrations.Clear();
            }
    
            internal class ChangeTrackerDisposable : IDisposable
            {
                private readonly Action<TOptions, string> _listener;
                private readonly OptionsMonitor<TOptions> _monitor;
    
                public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
                {
                    _listener = listener;
                    _monitor = monitor;
                }
    
                public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);
    
                public void Dispose() => _monitor._onChange -= OnChange;
            }
        }
    

    4. 测试代码

    本篇文章中,由于Option依赖于自带的注入系统,而本项目中Di部分还没有完成,所以,这篇文章的测试代码直接new依赖的对象。

    public class ConfigurationTest
    {
        public static void Run()
        {
            var builder = new ConfigurationBuilder();
            builder.AddJsonFile(null, $@"C:WorkStationCodeGitHubCodeCoreAppCoreWebAppappsettings.json", true,true);
            var configuration = builder.Build();
            Task.Run(() => {
                ChangeToken.OnChange(() => configuration.GetReloadToken(), () => {
                    Console.WriteLine("Configuration has changed");
                });
            });
            var optionsChangeTokenSource = new ConfigurationChangeTokenSource<MyOption>(configuration);
            var configureOptions = new NamedConfigureFromConfigurationOptions<MyOption>(string.Empty, configuration);
            var optionsFactory = new OptionsFactory<MyOption>(new List<IConfigureOptions<MyOption>>() { configureOptions },new List<IPostConfigureOptions<MyOption>>());
            var optionsMonitor = new OptionsMonitor<MyOption>(optionsFactory,new List<IOptionsChangeTokenSource<MyOption>>() { optionsChangeTokenSource },new OptionsCache<MyOption>());
            optionsMonitor.OnChange((option,name) => {
                Console.WriteLine($@"optionsMonitor Detected Configuration has changed,current Value is {option.TestOption}");
            });
            Thread.Sleep(600000);
        }
    }
    

    测试结果

    回调会触发两次,这是由于FileSystemWatcher造成的,可以通过设置一个后台线程,在检测到文件变化时,主线程将标志位置true,后台线程轮询标志位
    image

    结语

    至此,从IConfigurationTOptions强类型的映射已经完成。

  • 相关阅读:
    JS常用数值验证
    JS遍历对象的属性和值
    SpringBoot解决特殊符号 []报400问题
    postman工具的用法
    SpringBoot使用谷歌方式生成图片验证码
    hibernate配置多对多ORM映射关系
    hibernate配置一对多ORM映射关系
    Class文件结构
    垃圾收集器与内存分配策略
    hibernate持久化类和一级缓存
  • 原文地址:https://www.cnblogs.com/Kane-Blake/p/11426415.html
Copyright © 2020-2023  润新知