[KB]Xml序列化/反序列化時,碰到0x0B的問題

  • 8180
  • 0

[KB]Xml序列化/反序列化時,碰到0x0B的問題

前言

原有系統有許多中介資料,透過Xml來進行序列化與反序列化的處理。但前幾天發生了一個詭異的情況是:

  1. 接收到的Xml,進行反序列化失敗
  2. 檢查Xml資料,發現都只有一半的資料,也就是沒有結尾的tag,tag裡面的值,似乎也不完整,似乎內容寫到一半就被截斷了。

這實在是一個詭異的情況,按理來說,所產生的Xml應該都是透過XmlSerializer來序列化,要嘛序列化就會錯了,要嘛就應該產生完整的xml資料,怎麼會只有一半的。難道是寫檔到一半,網路中斷?

回過頭來檢查,最近網路中斷的情況並不常發生,而Xml資料只有產生一半的情況,卻在同一平台不同的程式中發生。交叉比對後發現,相同的pattern是同一間供應商所提供的資料,似乎有特殊符號,導致這樣的問題。在測試環境底下,查詢該供應商相關的資料,重現了一樣的錯誤情況,如圖:

image

要命的是,即使使用端發生了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()會直接產生檔案。

 

暫時解決方式

解決方式基本上有兩種:

  1. 過濾法:針對Xml吃不進去的字元進行過濾。
  2. 忽略法:把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資料如下:

image

我們透過測試程式,來模擬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相同,測試結果如下圖所示:

image

而產出的xml資料只有一半,如下圖所示:

image

接下來,將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序列化後會變成&#xB;,Xml輸出結果,如下圖所示:

image

反序列化後,Name屬性的值其實是"\v",如下圖:

image

 

結論

在做Xml序列化/反序列化時,還是有很多眉角要注意,更重要的是,如何在設計時便建立一套蒐集資訊的機制,用來還原問題原因與當時狀況。不然這種透過XmlSerializer產出的Xml資料只有一半的情況,真的是鬼擋牆了。

如果各位前輩,有其他更一勞永逸的方式,請不吝告知一下,眼前的解決方式,個人認為只是治標,而不是治本。不過這也帶出來,程式碼只有一份時,發生問題要改就輕鬆多了。


blog 與課程更新內容,請前往新站位置:http://tdd.best/