[KB]Xml序列化/反序列化時,碰到0x0B的問題
前言
原有系統有許多中介資料,透過Xml來進行序列化與反序列化的處理。但前幾天發生了一個詭異的情況是:
- 接收到的Xml,進行反序列化失敗
- 檢查Xml資料,發現都只有一半的資料,也就是沒有結尾的tag,tag裡面的值,似乎也不完整,似乎內容寫到一半就被截斷了。
這實在是一個詭異的情況,按理來說,所產生的Xml應該都是透過XmlSerializer來序列化,要嘛序列化就會錯了,要嘛就應該產生完整的xml資料,怎麼會只有一半的。難道是寫檔到一半,網路中斷?
回過頭來檢查,最近網路中斷的情況並不常發生,而Xml資料只有產生一半的情況,卻在同一平台不同的程式中發生。交叉比對後發現,相同的pattern是同一間供應商所提供的資料,似乎有特殊符號,導致這樣的問題。在測試環境底下,查詢該供應商相關的資料,重現了一樣的錯誤情況,如圖:
要命的是,即使使用端發生了exception,該Xml檔案還是會被輸出至filer上。而Xml內容只有寫入碰到0x0B之前的部份。所以,批次去讀Xml進行反序列化的時候,自然就會反序列化失敗。
原因
要了解原因,先來看原本系統上,序列化與反序列化的寫法(這邊Sample有做過簡化,以方便呈現):
public static class XmlConverter
{
public static T LoadFromString<T>(string data) where T : class
{
if (string.IsNullOrEmpty(data))
{
return default(T);
}
else
{
using (XmlReader inputStream = XmlReader.Create(new System.IO.StringReader(data)))
{
var factory = new XmlSerializer(typeof(T));
T result = factory.Deserialize(inputStream) as T;
return result;
}
}
}
public static void SaveToFile<T>(string filePath, T data) where T : class
{
using (XmlWriter writer = XmlWriter.Create(filePath))
{
var factory = new XmlSerializer(typeof(T));
factory.Serialize(writer, data);
}
}
}
因為序列化使用XmlWriter,所以針對XmlWriter查了一下MSDN,發現在Create的多載方法中,有一個參數XmlWriterSetting,而XmlWriterSetting中,有一個CheckCharacters的屬性,簡單的說,這一個屬性控制了XmlWriter是否支援字元檢查,檢查內容是否符合了合法Xml字元範圍。預設是true,也就是要檢查。而畫面上不曉得使用者怎麼輸入0x0B這類特殊符號(可能是貼powerpoint或是Shift + Enter這類特殊符號),所以導致XmlWriter在Create的過程會跳exception,而XmlWriter.Create()會直接產生檔案。
暫時解決方式
解決方式基本上有兩種:
- 過濾法:針對Xml吃不進去的字元進行過濾。
- 忽略法:把CheckCharacters設定為false。
這邊先使用第二種來解決現行系統的問題。
首先針對原本的程式,建立相關測試。(嚴格來說,並非單元測試)
private string filePath = @"C:\Users\joeychen\Desktop\joeytest.xml";
/// <summary>
///SaveToFile 的測試
///</summary>
public void SaveToFileTestHelper_正常資料<T>()
where T : class
{
//arrange
Person data = new Person { Name = "joey" };
//act
XmlConverter.SaveToFile<Person>(filePath, data);
//assert by code
var xml = XDocument.Load(filePath);
Debug.WriteLine("XML內容:");
Debug.WriteLine(xml);
var result = xml.Elements("Person").Elements("Name").First();
Assert.AreEqual("joey", result.Value);
}
[TestMethod()]
public void SaveToFileTest()
{
SaveToFileTestHelper_正常資料<GenericParameterHelper>();
}
這樣的test case,程式碼可以正常運作。Xml資料如下:
我們透過測試程式,來模擬0x0B的問題,只有模擬出一樣的問題,才能代表程式修改後,有解決問題,且不能影響原本正常的運作結果。特殊符號的test case如下:
private string filePath_specialSignal = @"C:\Users\joeychen\Desktop\joeytest_error.xml";
/// <summary>
///SaveToFile 的測試
///</summary>
public void SaveToFileTestHelper_特殊符號資料<T>()
where T : class
{
//arrange
int c = 0x0b;
char c2 = (char)c;
Person data = new Person() { Name = c2.ToString() };
//act
XmlConverter.SaveToFile<Person>(filePath_specialSignal, data);
}
[TestMethod()]
public void SaveToFileTest_特殊符號資料()
{
SaveToFileTestHelper_特殊符號資料<GenericParameterHelper>();
}
exception的資訊,就與頁面上的exception相同,測試結果如下圖所示:
而產出的xml資料只有一半,如下圖所示:
接下來,將XmlWriterSetting的CheckCharacters設為false,餵給Create。程式碼修改如下:
public static class XmlConverter
{
public static T LoadFromString<T>(string data) where T : class
{
if (string.IsNullOrEmpty(data))
{
return default(T);
}
else
{
var settings = new XmlReaderSettings { CheckCharacters = false };
using (XmlReader inputStream = XmlReader.Create(new System.IO.StringReader(data), settings))
{
var factory = new XmlSerializer(typeof(T));
T result = factory.Deserialize(inputStream) as T;
return result;
}
}
}
public static void SaveToFile<T>(string filePath, T data) where T : class
{
var setting = new XmlWriterSettings() { CheckCharacters = false };
using (XmlWriter writer = XmlWriter.Create(filePath, setting))
{
var factory = new XmlSerializer(typeof(T));
factory.Serialize(writer, data);
}
}
}
注意,反序列化的XmlReader的部份,也要用相同的設定,否則反序列化會產生問題。反序列化的測試程式如下:
/// <summary>
///LoadFromString 的測試
///</summary>
public void LoadFromStringTestHelper<T>()
where T : class
{
//arrange
string data = File.ReadAllText(filePath_specialSignal);
int c = 0x0b;
char c2 = (char)c;
Person expected = new Person() { Name = c2.ToString() };
//act
Person actual;
actual = XmlConverter.LoadFromString<Person>(data);
//assert
Assert.AreEqual(expected.Name, actual.Name);
}
[TestMethod()]
public void LoadFromStringTest()
{
LoadFromStringTestHelper<GenericParameterHelper>();
}
將CheckCharacters設為flase,0x0B序列化後會變成,Xml輸出結果,如下圖所示:
反序列化後,Name屬性的值其實是"\v",如下圖:
結論
在做Xml序列化/反序列化時,還是有很多眉角要注意,更重要的是,如何在設計時便建立一套蒐集資訊的機制,用來還原問題原因與當時狀況。不然這種透過XmlSerializer產出的Xml資料只有一半的情況,真的是鬼擋牆了。
如果各位前輩,有其他更一勞永逸的方式,請不吝告知一下,眼前的解決方式,個人認為只是治標,而不是治本。不過這也帶出來,程式碼只有一份時,發生問題要改就輕鬆多了。
blog 與課程更新內容,請前往新站位置:http://tdd.best/