有些天沒寫文章了,今晚給大家分享一下我對配置管理的實現思路。這個實現主要適合中小應用程序(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
