大叔手記(11):.Net配置文件的另類讀取方式
2011-12-20 09:21 湯姆大叔 閱讀(5909) 評論(14) 收藏 舉報前言
昨天看到博客園的Fish Li博友寫了一篇關于在.net中讀寫config文件的各種基本方法,甚是不錯,本著與大家分享學習的目的,現把我們項目中對XML格式基礎配置文件的讀取類與大家分享一下,希望對大家有所幫助。
FileWatcher的特點
通用類的名稱為FileWatcher,主要特點如下:
- 使用泛型以便能夠讀取不同類型的XML/Config配置文件,轉化成不同的實體類型
- 使用Lazy延遲讀取,也就是只有在用到的時候才讀,不用在Global里初始化
- 使用Func方便處理特定的邏輯
- 自動監控文件的更改變化
- 使用非常方便
用法
在看FileWatcher類源碼之前,我們先來看一下基本的用法,首先我們先來準備一些基礎數據,如下:
<?xml version="1.0" encoding="utf-8" ?>
<StateProvinceData xmlns="urn:com:company:project:v1">
<Country locale="ABW" hasStateProvinces="true" phonePrefix="297" />
<Country locale="ATG" hasStateProvinces="true" phonePrefix="1 268" />
<Country locale="USA" hasStateProvinces="true" phonePrefix="1" isPostalCodeRequired="true" >
<StateProvince value="Alabama">Alabama</StateProvince>
<StateProvince value="Alaska">Alaska</StateProvince>
<StateProvince value="Arizona">Arizona</StateProvince>
<StateProvince value="Texas">Texas</StateProvince>
<StateProvince value="Utah">Utah</StateProvince>
<StateProvince value="Vermont">Vermont</StateProvince>
<StateProvince value="Virginia">Virginia</StateProvince>
<StateProvince value="Washington">Washington</StateProvince>
<StateProvince value="West Virginia">West Virginia</StateProvince>
<StateProvince value="Wisconsin">Wisconsin</StateProvince>
<StateProvince value="Wyoming">Wyoming</StateProvince>
</Country>
<Country locale="UZB" hasStateProvinces="true" phonePrefix="998" />
<Country locale="VAT" hasStateProvinces="true" phonePrefix="39" />
</StateProvinceData>
主要是一個Country集合,每個Country下面有一些特殊的屬性以及所擁有的省,這里我們只是節選一段例子。下面我們肯先來創建對于的實例類:
[XmlRoot(Namespace="urn:com:company:project:v1")]
public class StateProvinceData
{
[XmlElement("Country")]
public Country[] Countries { get; set; }
}
[XmlType(Namespace = "urn:com:company:project:v1")]
public class Country
{
[XmlAttribute("locale")]
public string Locale { get; set; }
[XmlIgnore]
public string Name
{
get { return (this.Locale != null) ? CountryName.ResourceManager.GetString(this.Locale) : ""; }
// 這一點比較特殊,主要是從資源文件讀取內容,暫時忽略
}
[XmlAttribute("hasStateProvinces")]
public bool HasStateProvinces { get; set; }
[XmlAttribute("isPostalCodeRequired")]
public bool IsPostalCodeRequired { get; set; }
[XmlAttribute("phonePrefix")]
public string PhonePrefix { get; set; }
[XmlElement("StateProvince")]
public StateProvince[] StateProvinces { get; set; }
}
[XmlType(Namespace = "urn:com:company:project:v1")]
public class StateProvince
{
[XmlAttribute("value")]
public string Value;
[XmlText]
public string Text;
}
這3個類主要是對應XML文件里的節點元素,通過XML的namespace和XMLAttribute來確保能和XML里的內容對應上。
最后,我們來看一下基本用法:
public static class StateProvinceTable
{
// 通過Lazy達到延遲初始化的目的
// 其參數是一個CreateWatcher委托,用來加載數據
private static Lazy<FileWatcher<IDictionary<string, Country>>> dataWatcher =
new Lazy<FileWatcher<IDictionary<string, Country>>>(CreateWatcher);
//暴露數據給外部使用
public static IDictionary<string, Country> Data
{
get { return dataWatcher.Value.Value; }
}
private static FileWatcher<IDictionary<string, Country>> CreateWatcher()
{
// 傳入path和LoadData兩個參數
// 其中LoadData參數是Func<FileStream, T>類型的委托,T目前就是IDictionary<string, Country>
return new FileWatcher<IDictionary<string, Country>>
(HttpContext.Current.Server.MapPath("~/StateProvinceData.config"), LoadData);
}
// 用來將FileStream轉化成實體集合
private static IDictionary<string, Country> LoadData(FileStream configFile)
{
StateProvinceData dataTable = (StateProvinceData)
(new XmlSerializer(typeof(StateProvinceData))).Deserialize(configFile);
return dataTable.Countries.ToDictionary(item => item.Locale);
}
}
這樣,我們在任何地方通過引用StateProvinceTable.Data就可以使用了,不需要在Global里初始化加載,不需要自己擔心XML數據被更改,直接用就OK了,所有的事情FileWatcher類已經幫你做了。
通過上面的調用方式,我們可以猜測到,FileWatcher類提供的構造函數應該是類似FileWatcher(string path, Func<FileStream, T> loadData)這樣的代碼,也就是FileStream到實體的轉化還是自己類做,這樣的話好處是可以自己控制一些特殊的邏輯,文件監控的通用功能在FileWatcher類里實現,我們來看一下源代碼。
源碼
public class FileWatcher<T> : IDisposable where T : class
{
// 需要由配置文件轉化成的對象類型
private T value;
// 通過文件流轉化成實體的委托
private Func<FileStream, T> readMethod;
// 處理出錯的時候,日志記錄接口類型
private ILogger logger;
// 文件監控對象
private FileSystemWatcher fileWatcher;
// 加載文件或文件改變的時候觸發此事件
private Lazy<ManualResetEvent> fileChangedEvent;
// 最后一次load失敗的時間戳
private long lastFailedLoadTicks = DateTime.MaxValue.Ticks;
// 等待報告錯誤信息的時間
private static readonly TimeSpan FailedLoadGracePeriod = new TimeSpan(hours: 0, minutes: 1, seconds: 0);
// 第一次load所需要的最大時間,超過1分鐘就拋出異常
private const int InitialLoadTimeoutMilliseconds = 60000;
// 構造函數
public FileWatcher(string path, Func<FileStream, T> readMethod, ILogger logger = null)
{
if (path == null)
throw new ArgumentNullException("path");
if (readMethod == null)
throw new ArgumentNullException("readMethod");
this.Path = path;
this.readMethod = readMethod;
// KernelContainer.Kernel.Get是使用Ninject獲取接口的實例,博友使用測試的話,需要處理一下這個代碼
this.logger = logger ?? KernelContainer.Kernel.Get<ILogger>();
this.fileChangedEvent = new Lazy<ManualResetEvent>(Initialize);
}
// 資源回收相關的代碼
#region IDisposable Members
~FileWatcher()
{
this.Dispose(false);
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.fileWatcher.Dispose();
this.fileWatcher = null;
}
}
#endregion
public string Path { get; private set; }
// 所生成的T對象
public T Value
{
get
{
if (!this.fileChangedEvent.Value.WaitOne(InitialLoadTimeoutMilliseconds))
{
throw new TimeoutException(
String.Format("Failed to perform initial file load within {0} milliseconds: {1}",
InitialLoadTimeoutMilliseconds, this.Path));
}
return Interlocked.CompareExchange(ref this.value, null, null);
}
private set
{
Interlocked.Exchange(ref this.value, value);
this.OnChanged(null);
}
}
// T更新的時候觸發事件類型
public event EventHandler Changed;
// 觸發事件
private void OnChanged(EventArgs e)
{
if (this.Changed != null)
{
this.Changed(this, e);
}
}
private DateTime LastFailedLoadTime
{
get
{
long ticks = Interlocked.CompareExchange(ref this.lastFailedLoadTicks, 0, 0);
return new DateTime(ticks);
}
set
{
Interlocked.Exchange(ref this.lastFailedLoadTicks, value.Ticks);
}
}
// 初始化 file system watcher
[SecuritySafeCritical]
private ManualResetEvent Initialize()
{
ManualResetEvent initLoadEvent = new ManualResetEvent(false);
FileInfo filePath = new FileInfo(this.Path);
this.fileWatcher = new FileSystemWatcher(filePath.DirectoryName, filePath.Name);
this.fileWatcher.Changed += new FileSystemEventHandler(OnFileChanged);
// 第一次讀取的時候使用單獨的線程來load文件
ThreadPool.QueueUserWorkItem(s =>
{
this.UpdateFile();
this.fileWatcher.EnableRaisingEvents = true;
initLoadEvent.Set();
});
return initLoadEvent;
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
this.UpdateFile();
}
// 文件更新的時候處理方法
private void UpdateFile()
{
try
{
if (File.Exists(this.Path))
{
T newValue;
using (FileStream readStream = new FileStream(this.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
try
{
newValue = this.readMethod(readStream); // 這里使用的readmethod就是你傳入loadData自定義處理方法
}
catch (IOException)
{
// Error occurred at the file system level - handle with top
throw;
}
catch (Exception readMethodEx)
{
this.LastFailedLoadTime = DateTime.MaxValue;
this.logger.Error("FileWatcher.UpdateFile",
String.Format(CultureInfo.InvariantCulture, "Error occured while parsing a watched file: {0}", this.Path),
String.Empty, readMethodEx);
return;
}
}
this.LastFailedLoadTime = DateTime.MaxValue;
this.Value = newValue;
}
}
catch (IOException ioEx)
{
if (this.LastFailedLoadTime == DateTime.MaxValue)
{
this.LastFailedLoadTime = DateTime.UtcNow;
}
else if (this.LastFailedLoadTime == DateTime.MinValue)
{
return;
}
else if (DateTime.UtcNow - this.LastFailedLoadTime >= FailedLoadGracePeriod)
{
this.logger.Error("FileWatcher.UpdateFile",
String.Format(CultureInfo.InvariantCulture, "Unable to access watched file after ({0}): {1}", FailedLoadGracePeriod, this.Path),
String.Empty, ioEx);
this.LastFailedLoadTime = DateTime.MinValue;
}
}
catch (Exception ex)
{
this.LastFailedLoadTime = DateTime.MaxValue;
this.logger.Error("FileWatcher.UpdateFile",
String.Format(CultureInfo.InvariantCulture, "Unable to retrieve watched file: {0}", this.Path),
String.Empty, ex);
}
}
}
源碼的主要就是實現了文件監控,以及各種異常的處理和錯誤消息的處理機制,通過外部傳入的readMethod來將最終處理好的對象返回給T。
總結
該類主要使用了FileSystemWatcher,異常控制,事件Handler,委托等大家都熟知的方式組合起來來實現,個人感覺確實還可以,主要是我喜歡直接通過靜態變量來用這種方式,而不需要另外單獨寫初始化代碼。
不過這個類,不是大叔寫的,而是Onsite Team里的一個同事寫的(此人曾在微軟工作過11年),其實總結起來沒有什么比較特殊的知識,只是我們被平時的項目壓得沒時間思考而已,我相信博客園的一些有經驗的人如果有充分的時間也能想出更好的方式來。
同步與推薦
本文已同步至目錄索引:《大叔手記全集》
大叔手記:旨在記錄日常工作中的各種小技巧與資料(包括但不限于技術),如對你有用,請推薦一把,給大叔寫作的動力
浙公網安備 33010602011771號