Xml作為資料存儲的一種方式,當資料非常大的時候,我們将碰到很多Xml處理的問題。通常,我們對Xml檔案進行編輯的最直接的方式是将xml檔案加載到XmlDocument,在記憶體中來對XmlDocument進行修改,然後再儲存到磁盤中。這樣的話我們将不得不将整個XML document 加載到記憶體中,這明顯是不明智的(對于大資料XML檔案來說,記憶體将消耗很大,哥表示鴨梨很大)。下面我們将要講的是如何高效的增加内容(對象實體内容)到xml日志檔案中。
(一)設計概要
總體來說,我們将(通過代碼)建立兩種不同的檔案,第一種為Xml檔案,第二種為xml片段(txt檔案),如下圖所示:

我們通過如下的定義來使2個不同的檔案相關聯。
<!ENTITY yourEntityRefName SYSTEM "your xml fragement address(relative or obsolute address) ">
(二)xml檔案的生成
先來看下如何建立相關的xml檔案,代碼如下:
private static void InitXmlFile(string xmlLogFilePath, string xmlLogContentFileName, string entityRef)
{
string docType = string.Format("\n<!DOCTYPE XmlLogFile \n [ \n <!ENTITY {0} SYSTEM \"{1}\">\n ]>\n", entityRef, xmlLogContentFileName);
XmlWriterSettings wrapperSettings = new XmlWriterSettings()
{
Indent = true
};
using (XmlWriter writer = XmlWriter.Create(xmlLogFilePath, wrapperSettings))
writer.WriteStartDocument();
writer.WriteRaw(docType);
writer.WriteStartElement(ConfigResource.XmlLogFile);
writer.WriteStartElement(ConfigResource.XmlLogContent);
writer.WriteEntityRef(entityRef);
writer.WriteEndElement();
writer.Close();
}
}
對xml檔案内容的寫入主要通過XmlWriter來進行操作的。這個方法比較簡單,不再講解,看下我們通過這個方法生成的檔案内容:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE XmlLogFile
[
<!ENTITY Locations SYSTEM "XmlLogContentFile-20110220000120.txt">
]>
<XmlLogFile>
<XmlLogContent>&Locations;</XmlLogContent>
</XmlLogFile>
Locations 為實體引用名稱,與之相對應的為&Locations; 。
XmlLogContentFile-20110220000120.txt為Xml片段的檔案名稱,路徑是相對于XmlLogFile-20110220000120.xml的。
&Locations;相當于占位符的作用,将用XmlLogContentFile-20110220000120.txt檔案的内容來替換XmlLogFile-20110220000120.xml的&Locations;
(三)Xml片段檔案的生成
Xml片段檔案的生成過程思路為:通過System.IO.FileStream和System.Xml.XmlWriter在檔案的末尾處增加檔案的内容(效率較高,因為是直接在檔案的末尾添加的内容),内容格式為Xml,其中涉及到反射的部分内容。
private static void InitEntityRefFile(string xmlLogContentFilePath, object logObject, string entityRef)
using (FileStream fileStream = new FileStream(xmlLogContentFilePath, FileMode.Append,
FileAccess.Write, FileShare.Read))
XmlWriterSettings settings = new XmlWriterSettings()
{
ConformanceLevel = ConformanceLevel.Fragment,
Indent = true,
OmitXmlDeclaration = false
};
WriteContent(logObject, fileStream, settings);
private static void WriteContent(object logObject, FileStream fileStream, XmlWriterSettings settings)
using (XmlWriter writer = XmlWriter.Create(fileStream, settings))
Type type = logObject.GetType();
writer.WriteStartElement(type.Name);
writer.WriteAttributeString(ConfigResource.Id,logObject.GetHashCode().ToString());
if (logObject.GetType().IsPrimitive ||
(logObject.GetType() == typeof(string)))
writer.WriteElementString(logObject.GetType().Name, logObject.ToString());
}
else
PropertyInfo[] infos = type.GetProperties();
foreach (PropertyInfo info in infos)
{
if (ValidateProperty(info))
{
writer.WriteElementString(info.Name,
(info.GetValue(logObject, null) ?? string.Empty).ToString());
}
}
writer.WriteEndElement();
writer.WriteWhitespace("\n");
}
private static bool ValidateProperty(PropertyInfo info)
{
return info.CanRead && (info.PropertyType.IsPrimitive
|| (info.PropertyType == typeof(string))
|| (info.PropertyType == typeof(DateTime)
|| (info.PropertyType == typeof(DateTime?))));
}
代碼 writer.WriteAttributeString(ConfigResource.Id,logObject.GetHashCode().ToString());
第一行為該實體增加一個Id特性,采用對象的哈希值來進行指派,友善以後的單元測試(通過對象的哈希值來查找相應的Xml内容)。
餘下的幾行為:當實體的類型是基元類型或者字元串類型的時候,直接通過writer.WriteElementString()方法将類型名稱,實體對象值作為參數直接寫入xml片段檔案中。
否則 else
通過反射來擷取所有屬性相對應的值,其中屬性必須是可讀的,并且為(基元類型,string,DateTiem?,DateTime)其中一種(這個大家可以擴充一下相關功能)。
如下所示,我們通過基元類型float,字元串類型string,對象類型Error【Point為Error的屬性,不是(基元類型,string,DateTiem?,DateTime)其中一種】來進行測試。
XmlLogHelper.Write("string type sample");
XmlLogHelper.Write(3.3);
XmlLogHelper.Write(DateTime.Now);
Error error = new Error()
Time = DateTime.Now,
Point = new System.Drawing.Point(0, 0),
Description = "C# Error",
Level = 2,
Name = "Error"
XmlLogHelper.Write(error);
輸出内容如下:
(四)采用lock來避免異常的發生,其次特别要注意對資源的及時釋放。
private static readonly object lockObject = new object();
public static void Write(object logObject)
if (logObject == null)
return;
lock (lockObject)
Writing(logObject);
private static void Writing(object logObject)
string entityRef = ConfigResource.EntityRef;
string baseDirectory = InitDirectory();
string baseName = DateTime.Now.ToString("yyyyMMddHHmmss");
string xmlLogFilePath =Path.Combine(baseDirectory ,string.Format(ConfigResource.XmlLogFileName,baseName));
XmlLogHelper.XmlFilePath = xmlLogFilePath;
string xmlLogContentFileName = string.Format(ConfigResource.XmlLogContentFileName,baseName);
string xmlLogContentFilePath = Path.Combine(baseDirectory, xmlLogContentFileName);
if (!File.Exists(xmlLogFilePath))
InitXmlFile(xmlLogFilePath, xmlLogContentFileName, entityRef);
InitEntityRefFile(xmlLogContentFilePath, logObject, entityRef);
采用lock來避免同時對檔案進行操作,避免異常的發生,保證每次操作都是僅有一個在進行。
lock (lockObject)
采用using來及時釋放掉資源。
using (FileStream fileStream = new FileStream(xmlLogContentFilePath, FileMode.Append,
FileAccess.Write, FileShare.Read))
{
}
(五)單元測試
單元測試的主要代碼如下,主要是對Write()方法進行測試,如下:
[TestMethod()]
public void WriteTest()
DeleteFiles();//删除目錄下所有檔案,避免産生不必要的影響。
List<Error> errors = InitErrorData(9);
AssertXmlContent(errors);
private static void AssertXmlContent(List<Error> errors)
foreach (Error error in errors)
XmlLogHelper.Write(error);
XmlDocument doc = GetXmlDocument();
XmlNode node = doc.SelectSingleNode("//Error[@Id='" + error.GetHashCode().ToString() + "']");
Assert.IsTrue(node.Name == typeof(Error).Name);
string path = string.Format("//Error[@Id='{0}']//", error.GetHashCode().ToString());
XmlNode levelNode = doc.SelectSingleNode(path + "Level");
XmlNode nameNode = doc.SelectSingleNode(path + "Name");
XmlNode descriptionNode = doc.SelectSingleNode(path + "Description");
XmlNode timeNode = doc.SelectSingleNode(path + "Time");
XmlNode pointNode = doc.SelectSingleNode(path + "Point");
Assert.IsTrue(nameNode.Name == "Name");
Assert.IsTrue(levelNode.Name == "Level");
Assert.IsTrue(descriptionNode.Name == "Description");
Assert.IsTrue(timeNode.Name == "Time");
Assert.IsNotNull(levelNode);
Assert.IsNotNull(nameNode);
Assert.IsNotNull(descriptionNode);
Assert.IsNotNull(timeNode);
Assert.IsNull(pointNode);
Assert.IsTrue(nameNode.InnerText == (error.Name ?? string.Empty));
Assert.IsTrue(levelNode.InnerText == error.Level.ToString());
Assert.IsTrue(timeNode.InnerText == DateTime.MinValue.ToString());
Assert.IsTrue(descriptionNode.InnerText == (error.Description ?? string.Empty));
上面僅僅是針對一個自定義的Error類進行了驗證................
(六)其他應用
當我們的Xml日志檔案可以記錄的時候,我們可能想通過界面來看下效果,比如如下所示意的圖中,點選【生成XML日志檔案】,再點選【擷取XML日志檔案】的時候,我們能夠看到生成的XML日志檔案。
其中生成的檔案名稱顯示如下:
多次點選【生成XML日志檔案】,再點選【擷取XML日志檔案】的時候,我們能夠看到生成的XML日志檔案數量也遞增(因為我将檔案的名稱設定為string baseName = DateTime.Now.ToString("yyyyMMddHHmmss");,按照秒數來計算的)。點選任何一個檔案,将顯示該檔案的相關全部xml内容(包括xml檔案和xml片段)。
點選【删除XML日志檔案】将删除所有的xml檔案,如下:
(七)總結
對于流的操作來說,應盡快釋放掉系統資源,促使GC的Finalize()方法的執行,同時可以避免異常的發生。對于Xml日志來說,當資料量越來越大的時候,我們可以将内容分為兩部分,一部分為标準的哦xml檔案,另一部分為xml片段檔案。這樣,我們能夠在xml片段檔案中友善地在檔案末尾處增加相關的内容,這種效率是非常快的,而通常我們通過XMLDocument來加載資料非常消耗記憶體,效率較低(資料量越大越明顯)。同時在讀取xml檔案的時候也會通過實體引用将相關的xml片段引用進來,進而使二個檔案成為一個整體。再次,在将對象轉換成xml的時候,通過反射來擷取相關的資料,并将資料寫入xml格式中,這個地方還有提高。希望各位在看完此文後也能熟練的運用XML日志檔案來對日志進行記錄。
源代碼下載下傳:
Jasen.XmlLogFileSample.rar XML日志檔案源代碼下載下傳