怎麼設計個性化、靈活、實時更新的配置管理器?講講實現思路

有些天沒寫文章了,今晚給大家分享一下我對配置管理的實現思路。這個實現主要適合中小應用程序(Web或Winform),如果你的網站需要負載均衡,那這個方案就不適用了,這時建議配置保存在數據庫或分佈式緩存裏,如果你有更好的想法,歡迎指點。這個配置設計在09年開發SNS網站時就完成了,那時看了Discuz!的.net開源版本,覺得它的配置管理不夠靈活纔想到用泛型來實現自己的配置管理組件。今天要講的實現比09初版本多了兩點功能:配置路徑的遲加載和自定義配置序列化。其中路徑遲加載是在看到湯姆大叔一個配置管理文章想到的,其實這個小功能不需要.Net4.0的Lazy就可以輕鬆實現,因爲,需求太簡單了。

 

這裏所說的個性化、靈活、實時更新的定義?

個性化,是指你可以隨意定義自己想要的配置結構、保存格式、存放位置等等。

靈活,是指可以方便的對配置進行讀、寫操作,並可以很容易實現任意多個配置管理器。

實時更新,是指在配置發生改變時可以實時的更新,且不會重啓Web應用程序。

 

IFileConfigManager<T>

下面開始講解設計。既然是配置管理器,那還是先定義好接口吧,請看IFileConfigManager<T>:

    /// <summary>
    /// Interface containing all properties and methods to be implemented
    /// by file configuration manager.
    /// </summary>
    /// <typeparam name="T">The type of config entity.</typeparam>
    public interface IFileConfigManager<T> : IDisposable 
        where T : class, new()
    {
        /// <summary>
        /// Gets the path of the config file.
        /// </summary>
        string Path { get; }

        /// <summary>
        /// Gets the encoding to read or write the config file.
        /// </summary>
        Encoding Encoding { get; }

        /// <summary>
        /// Gets the serializer of the config manager for loading or saving the config file.
        /// </summary>
        FileConfigSerializer<T> Serializer { get; }

        /// <summary>
        /// Gets the current config entity.
        /// </summary>
        /// <returns></returns>
        T GetConfig();

        /// <summary>
        /// Saves the current config entity to file.
        /// </summary>
        void SaveConfig();

        /// <summary>
        /// Saves a specified config entity to file.
        /// </summary>
        /// <param name="config"></param>
        void SaveConfig(T config);

        /// <summary>
        /// Backups the current config entity to a specified path.
        /// </summary>
        /// <param name="backupPath"></param>
        void BackupConfig(string backupPath);

        /// <summary>
        /// Restores config entity from a specified path and saves to the current path.
        /// </summary>
        /// <param name="restorePath"></param>
        void RestoreConfig(string restorePath);
    }

 

T參數當然就是定義的配置類型了,而且必須是引用類型,有無參數構造函數。Path是配置文件的完整路徑,Encoding是讀取和保存配置時用的編碼,Serializer是處理配置序列化和反序列化的具體實現,GetConfig()是獲取當前配置,SaveConfig()是保存當前配置,SaveConfig(T config)是保存指定的配置,BackupConfig(string backupPath)備份配置到指定路徑,RestoreConfig(string restorePath)從指定路徑還原配置。

 

FileConfigSerializer<T>

接口IFileConfigManager<T>中定義的Serializer是用於支持自定義配置序列化功能的,下面看看FileConfigSerializer<T>的實現:

    public abstract class FileConfigSerializer<T> 
        where T : class, new()
    {
        #region Fields

        // XML格式
        public static readonly FileConfigSerializer<T> Xml = new XmlFileConfigSerializer();

        // 二進制格式
        public static readonly FileConfigSerializer<T> Binary = new BinaryFileConfigSerializer();

        #endregion

        #region Methods

        // 從配置文件反序列化,使用指定的編碼
        public abstract T DeserializeFromFile(string path, Encoding encoding);

        // 序列化到配置文件,使用指定的編碼
        public abstract void SerializeToFile(T config, string path, Encoding encoding);

        #endregion

        #region XmlFileConfigSerializer

        // 實現默認的Xml序列化類
        private sealed class XmlFileConfigSerializer : FileConfigSerializer<T> 
        {
            public override T DeserializeFromFile(string path, Encoding encoding)
            {
                return SerializationUtil.DeserializeFromXmlFile<T>(path, encoding);
            }

            public override void SerializeToFile(T config, string path, Encoding encoding)
            {
                SerializationUtil.SerializeToXmlFile(config, path, encoding);
            }
        }

        #endregion

        #region BinaryFileConfigSerializer

        // 實現默認的二進制序列化類
        private sealed class BinaryFileConfigSerializer : FileConfigSerializer<T>
        {
            public override T DeserializeFromFile(string path, Encoding encoding)
            {
                return SerializationUtil.DeserializeFromBinaryFile<T>(path, encoding);
            }

            public override void SerializeToFile(T config, string path, Encoding encoding)
            {
                SerializationUtil.SerializeToBinaryFile(config, path, encoding);
            }
        }

        #endregion
    }

 FileConfigSerializer<T>定義爲抽象類,是爲了方便默認的使用和擴展,裏面使用的SerializationUtil類,是本人爲了方便寫的一個簡單的序列化助手類,相信大家對對象的序列化操作不會陌生了,無非使用了System.Xml.Serialization.XmlSerializer、System.Runtime.Serialization.Formatters.Binary.BinaryFormatter、System.Runtime.Serialization.Json.DataContractJsonSerializer和System.Runtime.Serialization.NetDataContractSerializer來處理。如果不想用它們,你還可以實現FileConfigSerializer<T>進行完全的自己定義配置的加載與保存方式。對於json序列化推薦大家使用http://www.codeplex.com/json/ 。

 

序列化用到的四個函數實現如下:

        public static void SerializeToXmlFile(object obj, string path, Encoding encoding)
        {
            using (var sw = new StreamWriter(path, false, encoding))
            {
                new XmlSerializer(obj.GetType()).Serialize(sw, obj);
            }
        }

        public static object DeserializeFromXmlFile(string path, Type type, Encoding encoding)
        {
            object obj = null;

            using (var sr = new StreamReader(path, encoding))
            {
                using (var xtr = new XmlTextReader(sr))
                {
                    xtr.Normalization = false;
                    obj = new XmlSerializer(type).Deserialize(xtr);
                }
            }

            return obj;
        }

        public static void SerializeToBinaryFile(object obj, string path, Encoding encoding)
        {
            byte[] bytes = null;
            using (var ms = new MemoryStream())
            {
                new BinaryFormatter().Serialize(ms, obj);
                ms.Position = 0;
                bytes = new Byte[ms.Length];
                ms.Read(bytes, 0, bytes.Length);

                using (var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
                {
                    using (var bw = new BinaryWriter(fs, encoding))
                    {
                        bw.Write(bytes);
                    }
                }
            }
        }

        public static object DeserializeFromBinaryFile(string path, Encoding encoding)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                using (var br = new BinaryReader(fs, encoding))
                {
                    byte[] bytes = new byte[fs.Length];
                    br.Read(bytes, 0, (int)fs.Length);

                    using (var ms = new MemoryStream())
                    {
                        ms.Write(bytes, 0, bytes.Length);
                        ms.Position = 0;
                        return new BinaryFormatter().Deserialize(ms);
                    }
                }
            }
        }

 

 實時更新

好了,大家已經知道了接口的定義了,下面來講講實時更新配置功能有哪些方法可以實現。我們知道,如果利用Web.config來配置的話,第一:如果配置內容多而雜,那會很亂;第二:如果手動修改配置,會導致Web重啓(而我們並不希望它重啓),所以,如果要解決上面兩點問題,我們就要思考點什麼了。上面我提到了Discuz!論壇的.net開源版本里配置管理,它是使用Timer來定時查檢配置是否有修改,如果有修改就重新加載的,恩,這是一個可行的方案。還有其它方法嗎?必須是有的,只要你肯去思考,下面列出本人想到的幾個比較容易想到的方案:

方法1:使用Timer(.net庫裏有三個timer,請自行選擇),每隔一秒就查檢一下配置文件修改時間,如果文件被修改了,正更新最後修改時間並重新加載配置內容;

方法2:使用System.IO.FileSystemWatcher,可以實時監控配置文件,一發生改變即重新加載配置內容;

方法3:使用System.Web.Caching.Cache,加上緩存依賴,文件更改後緩存會失效,同樣可以實時重新加載配置內容。

這三種方法中,方法3是本人比較推薦的,因爲它的開銷最小,而且可以實時更新配置,實現起來也是最簡單的。對於新手可能看到這還不知道實現,下面再貼出本人實現上面接口的四個類,一個是默認管理器類,沒有實時更新的功能,其它三個就是實現上面三種方法的管理器類了。

    internal class DefaultFileConfigManager<T> : DisposableObject, IFileConfigManager<T>
        where T : class, new()
    {
        #region Fields

        private string path = null;
        private Func<string> pathCreator = null;

        #endregion

        #region Constructors

        public DefaultFileConfigManager(Func<string> pathCreator, FileConfigSerializer<T> serializer, Encoding encoding)
        {
            pathCreator.ThrowsIfNull("pathCreator");
            serializer.ThrowsIfNull("serializer");

            this.pathCreator = pathCreator;
            this.Encoding = encoding;
            this.Serializer = serializer;
            this.SyncRoot = new object();
            this.Config = null;
        }

        #endregion

        #region Properties

        public string Path
        {
            get
            {
                if (this.path == null)
                {
                    string path = this.pathCreator();

                    path.ThrowsIfNull("The path returned form pathCreator is null.");

                    this.path = path;
                    this.LazyInitialize();
                }

                return this.path;
            }
        }

        public Encoding Encoding
        {
            get;
            protected set;
        }

        public FileConfigSerializer<T> Serializer
        {
            get;
            protected set;
        }

        protected object SyncRoot
        {
            get;
            set;
        }

        protected virtual T Config
        {
            get;
            set;
        }

        #endregion

        #region Methods

        public virtual T GetConfig()
        {
            if (this.Config == null)
            {
                lock (this.SyncRoot)
                {
                    if (this.Config == null)
                    {
                        FileInfo file = new FileInfo(this.Path);
                        if (!file.Exists)
                        {
                            // make sure the existence of the config directory
                            if (!file.Directory.Exists)
                            {
                                file.Directory.Create();
                            }
                            // save the default config to file
                            this.Config = new T();
                            this.Serializer.SerializeToFile(this.Config, this.Path, this.Encoding);
                        }
                        else
                        {
                            // else, loads from the specified path
                            this.Config = this.Serializer.DeserializeFromFile(this.Path, this.Encoding);
                        }
                    }
                }
            }

            return this.Config;
        }

        public void SaveConfig()
        {
            this.SaveConfig(this.GetConfig());
        }

        public virtual void SaveConfig(T config)
        {
            config.ThrowsIfNull("config");

            lock (this.SyncRoot)
            {
                FileInfo file = new FileInfo(this.Path);

                // make sure the existence of the config directory
                if (!file.Directory.Exists)
                {
                    file.Directory.Create();
                }

                this.Config = config;
                this.Serializer.SerializeToFile(this.Config, this.Path, this.Encoding);
            }
        }

        public void BackupConfig(string backupPath)
        {
            backupPath.ThrowsIfNull("backupPath");

            T config = this.GetConfig();
            this.Serializer.SerializeToFile(config, backupPath, this.Encoding);
        }

        public void RestoreConfig(string restorePath)
        {
            restorePath.ThrowsIfNull("restorePath");

            T config = this.Serializer.DeserializeFromFile(restorePath, this.Encoding);
            this.SaveConfig(config);
        }

        // this method is provided to subclasses to initialize their data
        protected virtual void LazyInitialize()
        {
        }

        #endregion
    }

 

    internal sealed class FileConfigManagerWithTimer<T> : DefaultFileConfigManager<T>
        where T : class, new()
    {
        private Timer timer = null;
        private DateTime lastWriteTime = DateTime.MinValue; // a flag to notify us of the change config

        public FileConfigManagerWithTimer(Func<string> pathCreator, FileConfigSerializer<T> serializer, Encoding encoding)
            : base(pathCreator, serializer, encoding)
        {
        }

        protected override void LazyInitialize()
        {
            base.LazyInitialize();

            // initializes the timer, with it's interval of 1000 milliseconds
            this.timer = new Timer(1000);
            this.timer.Enabled = true;
            this.timer.AutoReset = true;
            this.timer.Elapsed += new ElapsedEventHandler(Timer_Elapsed);
            this.timer.Start();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // disposes the timer
                this.timer.Dispose();
                this.timer = null;
            }
        }

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (!File.Exists(this.Path))
            {
                // the file has been deleted
                return;
            }

            var tempWriteTime = File.GetLastWriteTime(this.Path);

            // if equals to the initial value, update it and return
            if (this.lastWriteTime == DateTime.MinValue)
            {
                this.lastWriteTime = tempWriteTime;
                return;
            }

            // if no equals to new write time, update it and reload config
            if (this.lastWriteTime != tempWriteTime)
            {
                this.lastWriteTime = tempWriteTime;

                lock (this.SyncRoot)
                {
                    this.Config = this.Serializer.DeserializeFromFile(this.Path, this.Encoding);
                }
            }
        }
    }

 

 

    internal sealed class FileConfigManagerWithFileWatcher<T> : DefaultFileConfigManager<T>
        where T : class, new()
    {
        private FileWatcher watcher = null;

        public FileConfigManagerWithFileWatcher(Func<string> pathCreator, FileConfigSerializer<T> serializer, Encoding encoding)
            : base(pathCreator, serializer, encoding)
        {
        }

        protected override void LazyInitialize()
        {
            base.LazyInitialize();

            // when the path is created, the watcher should be initialize at the same time
            watcher = new FileWatcher(this.Path, FileChanged);
            // just start watching the file
            watcher.StartWatching();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // disposes the watcher
                this.watcher.Dispose();
                this.watcher = null;
            }

            base.Dispose(disposing);
        }

        private void FileChanged(object sender, FileSystemEventArgs args)
        {
            lock (this.SyncRoot)
            {
                this.watcher.StopWatching();
                try
                {
                    // note: here making the cuurent thread sleeping a litle while to avoid exception throwed by watcher
                    Thread.Sleep(10);
                    // reload the config from file
                    this.Config = this.Serializer.DeserializeFromFile(this.Path, this.Encoding);
                }
                catch (Exception)
                {
                    // ignore it
                }
                finally
                {
                    this.watcher.StartWatching();
                }
            }
        }
    }

 

    internal sealed class FileConfigManagerWithCacheDependency<T> : DefaultFileConfigManager<T>
        where T : class, new()
    {
        const string KeyPrefix = "FileConfig:";

        public FileConfigManagerWithCacheDependency(Func<string> pathCreator, FileConfigSerializer<T> serializer, Encoding encoding)
            : base(pathCreator, serializer, encoding)
        {
        }

        protected override T Config
        {
            get
            {
                return HttpRuntime.Cache[KeyPrefix + this.Path] as T;
            }
            set
            {
                // if not null, update the cache value
                if (value != null)
                {
                    HttpRuntime.Cache.Insert(KeyPrefix + this.Path, value, new CacheDependency(this.Path), DateTime.Now.AddYears(1), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
                }
            }
        }
    }

 

這裏值得講一下的是,默認管理器DefaultFileConfigManager<T>的Func<string> pathCreator參數,這個是爲了實現配置文件的遲加載的,有了它,就不需要在靜態構造函數或Global.asax裏初始化管理器實例了,另外爲了方便使用,本人還另寫了個類爲返回創建的管理器實例,這個就沒什麼好說的了,這也就是爲什麼上面幾個類的訪問範圍是程序集內部的。到此,整個實現的思路和大部分的代碼實現都講完了,希望對大家有所幫助:) 更多請關注: KudyStudio文章目錄

 

  有興趣的朋友還可以下載這個例子看看各種方法下的效果 : FileConfigWeb.rar

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章