有些天没写文章了,今晚给大家分享一下我对配置管理的实现思路。这个实现主要适合中小应用程序(Web或Winform),如果你的网站需要负载均衡,那这个方案就不适用了,这时建议配置保存在数据库或分布式缓存里,如果你有更好的想法,欢迎指点。这个配置设计在09年开发SNS网站时就完成了,那时看了Discuz!的.net开源版本,觉得它的配置管理不够灵活才想到用泛型来实现自己的配置管理组件。今天要讲的实现比09初版本多了两点功能:配置路径的迟加载和自定义配置序列化。其中路径迟加载是在看到汤姆大叔一个配置管理文章想到的,其实这个小功能不需要.Net4.0的Lazy就可以轻松实现,因为,需求太简单了。
个性化,是指你可以随意定义自己想要的配置结构、保存格式、存放位置等等。
灵活,是指可以方便的对配置进行读、写操作,并可以很容易实现任意多个配置管理器。
实时更新,是指在配置发生改变时可以实时的更新,且不会重启Web应用程序。
下面开始讲解设计。既然是配置管理器,那还是先定义好接口吧,请看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)从指定路径还原配置。
接口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