打开DoRefresh,供使用方刷新
大石头 编写于 2022-03-06 22:03:28
X
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Remoting;
using NewLife.Serialization;
using NewLife.Threading;

namespace NewLife.Configuration
{
    /// <summary>配置中心提供者</summary>
    public class HttpConfigProvider : ConfigProvider
    {
        #region 属性
        /// <summary>服务器</summary>
        public String Server { get; set; }

        /// <summary>服务操作 默认:Config/GetAll</summary>
        public String Action { get; set; } = "Config/GetAll";

        /// <summary>应用标识</summary>
        public String AppId { get; set; }

        /// <summary>应用密钥</summary>
        public String Secret { get; set; }

        /// <summary>实例。应用可能多实例部署,ip@proccessid</summary>
        public String ClientId { get; set; }

        /// <summary>作用域。获取指定作用域下的配置值,生产、开发、测试 等</summary>
        public String Scope { get; set; }

        /// <summary>命名空间。Apollo专用,多个命名空间用逗号或分号隔开</summary>
        public String NameSpace { get; set; }

        /// <summary>本地缓存配置数据。即使网络断开,仍然能够加载使用本地数据,默认Encrypted</summary>
        public ConfigCacheLevel CacheLevel { get; set; } = ConfigCacheLevel.Encrypted;

        /// <summary>更新周期。默认60秒,0秒表示不做自动更新</summary>
        public Int32 Period { get; set; } = 60;

        /// <summary>Api客户端</summary>
        public IApiClient Client { get; set; }

        /// <summary>服务器信息。配置中心最后一次接口响应,包含配置数据以外的其它内容</summary>
        public IDictionary<String, Object> Info { get; set; }

        private Int32 _version;
        private IDictionary<String, Object> _cache;
        #endregion

        #region 构造
        /// <summary>实例化Http配置提供者,对接星尘和阿波罗等配置中心</summary>
        public HttpConfigProvider()
        {
            try
            {
                var executing = AssemblyX.Create(Assembly.GetExecutingAssembly());
                var asm = AssemblyX.Entry ?? executing;
                if (asm != null) AppId = asm.Name;

                ValidClientId();
            }
            catch { }
        }

        private void ValidClientId()
        {
            try
            {
                // 刚启动时可能还没有拿到本地IP
                if (ClientId.IsNullOrEmpty() || ClientId[0] == '@')
                    ClientId = $"{NetHelper.MyIP()}@{Process.GetCurrentProcess().Id}";
            }
            catch { }
        }

        /// <summary>销毁</summary>
        /// <param name="disposing"></param>
        protected override void Dispose(Boolean disposing)
        {
            base.Dispose(disposing);

            _timer.TryDispose();
            Client.TryDispose();
        }

        /// <summary>已重载。输出友好信息</summary>
        /// <returns></returns>
        public override String ToString() => $"{GetType().Name} AppId={AppId} Server={Server}";
        #endregion

        #region 方法
        private IApiClient GetClient()
        {
            if (Client == null)
            {
                Client = new ApiHttpClient(Server)
                {
                    Timeout = 3_000
                };
            }

            return Client;
        }

        /// <summary>设置阿波罗服务端</summary>
        /// <param name="nameSpaces">命名空间。多个命名空间用逗号或分号隔开</param>
        public void SetApollo(String nameSpaces = "application") => NameSpace = nameSpaces;

        /// <summary>从本地配置文件读取阿波罗地址,并得到阿波罗配置提供者</summary>
        /// <param name="fileName">阿波罗配置文件名,默认appsettings.json</param>
        /// <param name="path">加载路径,默认apollo</param>
        /// <returns></returns>
        public static HttpConfigProvider LoadApollo(String fileName = null, String path = "apollo")
        {
            if (fileName.IsNullOrEmpty()) fileName = "appsettings.json";
            if (path.IsNullOrEmpty()) path = "apollo";

            // 读取本地配置,得到Apollo地址后,加载全部配置
            var jsonConfig = new JsonConfigProvider { FileName = fileName };
            var apollo = jsonConfig.Load<ApolloModel>(path);
            if (apollo == null) return null;

            var httpConfig = new HttpConfigProvider { Server = apollo.MetaServer.EnsureStart("http://"), AppId = apollo.AppId };
            httpConfig.SetApollo("application," + apollo.NameSpace);
            if (!httpConfig.Server.IsNullOrEmpty() && !httpConfig.AppId.IsNullOrEmpty()) httpConfig.LoadAll();

            return httpConfig;
        }

        private class ApolloModel
        {
            public String WMetaServer { get; set; }

            public String AppId { get; set; }

            public String NameSpace { get; set; }

            public String MetaServer { get; set; }
        }

        /// <summary>获取所有配置</summary>
        /// <returns></returns>
        protected virtual IDictionary<String, Object> GetAll()
        {
            var client = GetClient() as ApiHttpClient;

            // 特殊处理Apollo
            if (!NameSpace.IsNullOrEmpty())
            {
                var ns = NameSpace.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries).Distinct();
                var dic = new Dictionary<String, Object>();
                foreach (var item in ns)
                {
                    var action = $"/configfiles/json/{AppId}/default/{item}";
                    var rs = client.Get<IDictionary<String, Object>>(action);
                    foreach (var elm in rs)
                    {
                        if (!dic.ContainsKey(elm.Key)) dic[elm.Key] = elm.Value;
                    }
                }
                Info = dic;

                return dic;
            }
            else
            {
                ValidClientId();

                var rs = client.Post<IDictionary<String, Object>>(Action, new
                {
                    appId = AppId,
                    secret = Secret,
                    clientId = ClientId,
                    scope = Scope,
                    version = _version,
                    usedKeys = UsedKeys.Join(),
                    missedKeys = MissedKeys.Join(),
                });
                Info = rs;

                // 增强版返回
                if (rs.TryGetValue("configs", out var obj))
                {
                    var ver = rs["version"].ToInt(-1);
                    if (ver > 0) _version = ver;

                    if (obj is not IDictionary<String, Object> configs) return null;

                    return configs;
                }

                return rs;
            }
        }

        /// <summary>设置配置项,保存到服务端</summary>
        /// <param name="configs"></param>
        /// <returns></returns>
        protected virtual Int32 SetAll(IDictionary<String, Object> configs)
        {
            // 特殊处理Apollo
            if (!NameSpace.IsNullOrEmpty()) throw new NotSupportedException("Apollo不支持保存配置!");

            ValidClientId();

            var client = GetClient() as ApiHttpClient;

            return client.Post<Int32>("Config/SetAll", new
            {
                appId = AppId,
                secret = Secret,
                clientId = ClientId,
                configs,
            });
        }

        /// <summary>初始化提供者,如有必要,此时加载缓存文件</summary>
        /// <param name="value"></param>
        public override void Init(String value)
        {
            // 本地缓存
            var file = (value.IsNullOrWhiteSpace() ? $"Config/httpConfig_{AppId}.json" : $"{value}_{AppId}.json").GetFullPath();
            if ((Root == null || Root.Childs.Count == 0) && CacheLevel > ConfigCacheLevel.NoCache && File.Exists(file))
            {
                var json = File.ReadAllText(file);

                // 加密存储
                if (CacheLevel == ConfigCacheLevel.Encrypted) json = Aes.Create().Decrypt(json.ToBase64(), AppId.GetBytes()).ToStr();

                Root = Build(JsonParser.Decode(json));
            }
        }

        /// <summary>加载配置字典为配置树</summary>
        /// <param name="configs"></param>
        /// <returns></returns>
        public virtual IConfigSection Build(IDictionary<String, Object> configs)
        {
            // 换个对象,避免数组元素在多次加载后重叠
            var root = new ConfigSection { };
            foreach (var item in configs)
            {
                var ks = item.Key.Split(':');
                var section = root;
                for (var i = 0; i < ks.Length; i++)
                {
                    section = section.GetOrAddChild(ks[i]) as ConfigSection;
                }

                //var section = root.GetOrAddChild(key);
                if (item.Value is IDictionary<String, Object> dic)
                    section.Childs = Build(dic).Childs;
                else
                    section.Value = item.Value + "";
            }
            return root;
        }

        private Int32 _inited;
        /// <summary>加载配置</summary>
        public override Boolean LoadAll()
        {
            try
            {
                // 首次访问,加载配置
                if (_inited == 0 && Interlocked.CompareExchange(ref _inited, 1, 0) == 0)
                    Init(null);
            }
            catch { }

            try
            {
                IsNew = true;

                var dic = GetAll();
                if (dic != null)
                {
                    if (dic.Count > 0) IsNew = false;

                    Root = Build(dic);

                    // 缓存
                    SaveCache(dic);
                }

                // 自动更新
                if (Period > 0) InitTimer();

                return true;
            }
            catch (Exception ex)
            {
                XTrace.WriteException(ex);

                return false;
            }
        }

        private void SaveCache(IDictionary<String, Object> configs)
        {
            // 缓存
            _cache = configs;

            // 本地缓存
            if (CacheLevel > ConfigCacheLevel.NoCache)
            {
                var file = $"Config/httpConfig_{AppId}.json".GetFullPath();
                var json = configs.ToJson();

                // 加密存储
                if (CacheLevel == ConfigCacheLevel.Encrypted) json = Aes.Create().Encrypt(json.GetBytes(), AppId.GetBytes()).ToBase64();

                File.WriteAllText(file.EnsureDirectory(true), json);
            }
        }

        /// <summary>保存配置树到数据源</summary>
        public override Boolean SaveAll()
        {
            var dic = new Dictionary<String, Object>();
            foreach (var item in Root.Childs)
            {
                if (item.Childs == null || item.Childs.Count == 0)
                {
                    // 只提交修改过的设置
                    if (_cache == null || !_cache.TryGetValue(item.Key, out var v) || v + "" != item.Value + "")
                    {
                        if (item.Comment.IsNullOrEmpty())
                            dic[item.Key] = item.Value;
                        else
                            dic[item.Key] = new { item.Value, item.Comment };
                    }
                }
                else
                {
                    foreach (var elm in item.Childs)
                    {
                        // 最多只支持两层
                        if (elm.Childs != null && elm.Childs.Count > 0) continue;

                        var key = $"{item.Key}:{elm.Key}";

                        // 只提交修改过的设置
                        if (_cache == null || !_cache.TryGetValue(key, out var v) || v + "" != elm.Value + "")
                        {
                            if (elm.Comment.IsNullOrEmpty())
                                dic[key] = elm.Value;
                            else
                                dic[key] = new { elm.Value, elm.Comment };
                        }
                    }
                }
            }

            if (dic.Count > 0) return SetAll(dic) >= 0;

            return true;
        }
        #endregion

        #region 绑定
        /// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
        /// <typeparam name="T">模型</typeparam>
        /// <param name="model">模型实例</param>
        /// <param name="autoReload">是否自动更新。默认true</param>
        /// <param name="path">路径。配置树位置,配置中心等多对象混合使用时</param>
        public override void Bind<T>(T model, Boolean autoReload = true, String path = null)
        {
            base.Bind<T>(model, autoReload, path);

            if (autoReload) InitTimer();
        }
        #endregion

        #region 定时
        /// <summary>定时器</summary>
        protected TimerX _timer;
        private void InitTimer()
        {
            if (_timer != null) return;
            lock (this)
            {
                if (_timer != null) return;

                var p = Period;
                if (p <= 0) p = 60;
                _timer = new TimerX(DoRefresh, null, p * 1000, p * 1000) { Async = true };
            }
        }

        /// <summary>定时刷新配置</summary>
        /// <param name="state"></param>
        protected void DoRefresh(Object state)
        {
            var dic = GetAll();
            if (dic == null) return;

            var keys = new List<String>();
            if (_cache != null)
            {
                foreach (var item in dic)
                {
                    if (!_cache.TryGetValue(item.Key, out var v) || v + "" != item.Value + "")
                    {
                        keys.Add(item.Key);
                    }
                }
            }

            if (keys.Count > 0)
            {
                XTrace.WriteLine("[{0}]配置改变,重新加载如下键:{1}", AppId, keys.Join());

                Root = Build(dic);

                // 缓存
                SaveCache(dic);

                NotifyChange();
            }
        }
        #endregion
    }
}