代码改变世界

大叔手记(11):.Net配置文件的另类读取方式

2011-12-20 09:21  汤姆大叔  阅读(5604)  评论(14编辑  收藏

前言

昨天看到博客园的Fish Li博友写了一篇关于在.net中读写config文件的各种基本方法,甚是不错,本着与大家分享学习的目的,现把我们项目中对XML格式基础配置文件的读取类与大家分享一下,希望对大家有所帮助。

FileWatcher的特点

通用类的名称为FileWatcher,主要特点如下:

  1. 使用泛型以便能够读取不同类型的XML/Config配置文件,转化成不同的实体类型
  2. 使用Lazy延迟读取,也就是只有在用到的时候才读,不用在Global里初始化
  3. 使用Func方便处理特定的逻辑
  4. 自动监控文件的更改变化
  5. 使用非常方便

用法

在看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年),其实总结起来没有什么比较特殊的知识,只是我们被平时的项目压得没时间思考而已,我相信博客园的一些有经验的人如果有充分的时间也能想出更好的方式来。

同步与推荐

本文已同步至目录索引:《大叔手记全集》

大叔手记:旨在记录日常工作中的各种小技巧与资料(包括但不限于技术),如对你有用,请推荐一把,给大叔写作的动力