9.8.2018.0630
大石头 authored at 2018-06-30 11:15:32
12.71 KiB
X
using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Web.Script.Serialization;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Serialization;
using NewLife.Threading;

namespace NewLife.Json
{
    /// <summary>Json配置文件基类</summary>
    /// <remarks>
    /// 标准用法:TConfig.Current
    /// 
    /// 配置实体类通过<see cref="JsonConfigFileAttribute"/>特性指定配置文件路径以及自动更新时间。
    /// Current将加载配置文件,如果文件不存在或者加载失败,将实例化一个对象返回。
    /// 
    /// 考虑到自动刷新,不提供LoadFile和SaveFile等方法,可通过扩展方法ToXmlFileEntity和ToXmlFile实现。
    /// 
    /// 用户也可以通过配置实体类的静态构造函数修改基类的<see cref="_.ConfigFile"/>和<see cref="_.ReloadTime"/>来动态配置加载信息。
    /// </remarks>
    /// <typeparam name="TConfig"></typeparam>
    public class JsonConfig<TConfig> : DisposeBase where TConfig : JsonConfig<TConfig>, new()
    {
        #region 静态
        private static Boolean _loading;
        private static TConfig _Current;
        /// <summary>当前实例。通过置空可以使其重新加载。</summary>
        public static TConfig Current
        {
            get
            {
                if (_loading) return _Current ?? new TConfig();

                var dcf = _.ConfigFile;
                if (dcf == null) return new TConfig();

                // 这里要小心,避免_Current的null判断完成后,_Current被别人置空,而导致这里返回null
                var config = _Current;
                if (config != null)
                {
                    // 现存有对象,尝试再次加载,可能因为未修改而返回null,这样只需要返回现存对象即可
                    if (!config.IsUpdated) return config;

                    XTrace.WriteLine("{0}的配置文件{1}有更新,重新加载配置!", typeof(TConfig), config.ConfigFile);

                    // 异步更新
                    ThreadPool.QueueUserWorkItem(s => config.Load(dcf));

                    return config;
                }

                // 现在没有对象,尝试加载,若返回null则实例化一个新的
                lock (dcf)
                {
                    if (_Current != null) return _Current;

                    config = new TConfig();
                    _Current = config;
                    if (!config.Load(dcf))
                    {
                        config.ConfigFile = dcf.GetFullPath();
                        config.SetExpire();  // 设定过期时间
                        config.IsNew = true;
                        config.OnNew();

                        config.OnLoaded();

                        // 创建或覆盖
                        var act = File.Exists(dcf.GetFullPath()) ? "加载出错" : "不存在";
                        XTrace.WriteLine("{0}的配置文件{1} {2},准备用默认配置覆盖!", typeof(TConfig).Name, dcf, act);
                        try
                        {
                            // 根据配置,有可能不保存,直接返回默认
                            if (_.SaveNew) config.Save();
                        }
                        catch (Exception ex)
                        {
                            XTrace.WriteException(ex);
                        }
                    }
                }

                return config;
            }
            set { _Current = value; }
        }

        /// <summary>一些设置。派生类可以在自己的静态构造函数中指定</summary>
        public static class _
        {
            /// <summary>是否调试</summary>
            public static Boolean Debug { get; set; }

            /// <summary>配置文件路径</summary>
            public static String ConfigFile { get; set; }

            /// <summary>重新加载时间。单位:毫秒</summary>
            public static Int32 ReloadTime { get; set; }

            /// <summary>没有配置文件时是否保存新配置。默认true</summary>
            public static Boolean SaveNew { get; set; } = true;

            static _()
            {
                // 获取JsonConfigFileAttribute特性,那里会指定配置文件名称
                var att = typeof(TConfig).GetCustomAttribute<JsonConfigFileAttribute>(true);
                if (att == null || att.FileName.IsNullOrWhiteSpace())
                {
                    // 这里不能着急,派生类可能通过静态构造函数指定配置文件路径
                    //throw new XException("编码错误!请为配置类{0}设置{1}特性,指定配置文件!", typeof(TConfig), typeof(XmlConfigFileAttribute).Name);
#if !__MOBILE__
                    _.ConfigFile = "ViewConfig\\{0}.json".F(typeof(TConfig).Name);
                    _.ReloadTime = 10000;
#endif
                }
                else
                {
                    _.ConfigFile = att.FileName;
                    _.ReloadTime = att.ReloadTime;
                }

                // 实例化一次,用于触发派生类中可能的静态构造函数
                var config = new TConfig();
            }
        }
        #endregion

        #region 属性
        /// <summary>配置文件</summary>
        [ScriptIgnore]
        public String ConfigFile { get; set; }

        /// <summary>最后写入时间</summary>
        [ScriptIgnore]
        private DateTime lastWrite;
        /// <summary>过期时间。如果在这个时间之后再次访问,将检查文件修改时间</summary>
        [ScriptIgnore]
        private DateTime expire;

        /// <summary>是否已更新。通过文件写入时间判断</summary>
        [ScriptIgnore]
        protected Boolean IsUpdated
        {
            get
            {
                var cf = ConfigFile;
                //if (cf.IsNullOrEmpty() || !File.Exists(cf)) return false;
                // 频繁调用File.Exists的性能损耗巨大
                if (cf.IsNullOrEmpty()) return false;

                var now = TimerX.Now;
                if (_.ReloadTime > 0 && expire < now)
                {
                    var fi = new FileInfo(cf);
                    fi.Refresh();
                    expire = now.AddMilliseconds(_.ReloadTime);

                    if (lastWrite < fi.LastWriteTime)
                    {
                        lastWrite = fi.LastWriteTime;
                        return true;
                    }
                }
                return false;
            }
        }

        /// <summary>设置过期重新加载配置的时间</summary>
        void SetExpire()
        {
            if (_.ReloadTime > 0)
            {
                // 这里必须在加载后即可设置过期时间和最后写入时间,否则下一次访问的时候,IsUpdated会报告文件已更新
                var fi = new FileInfo(ConfigFile);
                if (fi.Exists)
                {
                    fi.Refresh();
                    lastWrite = fi.LastWriteTime;
                }
                else
                    lastWrite = TimerX.Now;
                expire = TimerX.Now.AddMilliseconds(_.ReloadTime);
            }
        }

        /// <summary>是否新的配置文件</summary>
        [ScriptIgnore]
        public Boolean IsNew { get; set; }
        #endregion

        #region 构造
        /// <summary>销毁</summary>
        /// <param name="disposing"></param>
        protected override void OnDispose(Boolean disposing)
        {
            base.OnDispose(disposing);

            _Timer.TryDispose();
        }
        #endregion

        #region 加载
        /// <summary>加载指定配置文件</summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        public virtual Boolean Load(String filename)
        {
            if (filename.IsNullOrWhiteSpace()) return false;
            filename = filename.GetFullPath();
            if (!File.Exists(filename)) return false;

            _loading = true;
            try
            {
                var data = File.ReadAllBytes(filename);
                var config = this as TConfig;


                //var json = new Serialization.Json
                //{
                //    Stream = new MemoryStream(data),
                //    UseProperty = true,
                //    Indented = true
                //};

                //if (_.Debug) json.Log = XTrace.Log;

                //Object obj = config;
                var cfg = new FastJson().Read(data.ToStr(), config.GetType());
                config.Copy(cfg);

                //if (!json.TryRead(GetType(), ref obj)) return false;

                config.ConfigFile = filename;
                config.SetExpire();  // 设定过期时间
                config.OnLoaded();

                return true;
            }
            catch (Exception ex)
            {
                XTrace.WriteException(ex);
                return false;
            }
            finally
            {
                _loading = false;
            }
        }
        #endregion

        #region 成员方法
        /// <summary>从配置文件中读取完成后触发</summary>
        protected virtual void OnLoaded()
        {
            // 如果默认加载后的配置与保存的配置不一致,说明可能配置实体类已变更,需要强制覆盖
            var config = this;
            try
            {
                var cfi = ConfigFile;
                // 新建配置不要检查格式
                var flag = File.Exists(cfi);
                if (!flag) return;

                var json1 = File.ReadAllText(cfi).Trim();
                var json2 = config.GetJson().Trim();
                flag = json1 == json2;

                if (!flag)
                {
                    // 异步处理,避免加载日志路径配置时死循环
                    XTrace.WriteLine("配置文件{0}格式不一致,保存为最新格式!", cfi);
                    config.Save();
                }
            }
            catch (Exception ex)
            {
                if (_.Debug) XTrace.WriteException(ex);
            }
        }

        /// <summary>保存到配置文件中去</summary>
        /// <param name="filename"></param>
        public virtual void Save(String filename)
        {
            if (filename.IsNullOrWhiteSpace()) filename = ConfigFile;
            if (filename.IsNullOrWhiteSpace()) throw new XException("未指定{0}的配置文件路径!", typeof(TConfig).Name);
            filename = filename.GetFullPath();

            // 加锁避免多线程保存同一个文件冲突
            lock (filename)
            {
                var json1 = File.Exists(filename) ? File.ReadAllText(filename).Trim() : null;
                var json2 = GetJson();

                //if (File.Exists(filename)) File.Delete(filename);
                filename.EnsureDirectory(true);

                if (json1 != json2) File.WriteAllText(filename, json2);
            }
        }

        /// <summary>保存到配置文件中去</summary>
        public virtual void Save() => Save(null);

        private TimerX _Timer;
        /// <summary>异步保存</summary>
        public virtual void SaveAsync()
        {
            if (_Timer == null)
            {
                lock (this)
                {
                    if (_Timer == null) _Timer = new TimerX(DoSave, null, 1000, 5000)
                    {
                        Async = true,
                        CanExecute = () => _commits > 0,
                    };
                }
            }

            Interlocked.Increment(ref _commits);
        }

        private Int32 _commits;
        private void DoSave(Object state)
        {
            var old = _commits;
            //if (Interlocked.CompareExchange(ref _commits, 0, old) != old) return;
            if (old == 0) return;

            Save(null);

            Interlocked.Add(ref _commits, -old);
        }

        /// <summary>新创建配置文件时执行</summary>
        protected virtual void OnNew() { }

        private String GetJson()
        {
            var json = new NewLife.Serialization.Json
            {
                Encoding = Encoding.UTF8,
                UseProperty = true
            };

            if (_.Debug) json.Log = XTrace.Log;

            return this.ToJson(true);
        }
        #endregion
    }
}