閱讀710 返回首頁    go 阿裏雲 go 技術社區[雲棲]


一個關於解決序列化問題的編程技巧

前一篇文章中我曾經說過,現在正在做一個小小的框架以實現采用統一的API實現對上下文(Context)信息的統一管理。這個框架同時支持Web和GUI應用,並支持跨線程傳遞和跨域傳遞(這裏指在WCF服務調用中實現客戶端到服務端隱式傳遞),以及對上下文項目(ContextItem)的讀寫控製。關鍵就在於後麵兩個特性的支持上麵,出現一個小小的關於序列化的問題。解決方案隻需要改動短短的一行代碼,結果卻讓我折騰了老半天。

一、問題重現

為了重現我實際遇到的問題,我特意將問題簡化,為此我寫了一個簡單的例子(你可以從這裏下載)。在下麵的代碼片斷中,我創建了一個名稱為ContextItem的類型,代表一個需要維護的上下文項。由於需要在WCF服務調用實現自動傳遞,我將起定義成DataContract。ContextItem包含Key,Value和ReadOnly三個屬性,不用說ReadOnly表示該ContextItem可以被修改。注意Value屬性Set方法的定義——如果ReadOnly則拋出異常。

   1: [DataContract(Namespace = "https://www.artech.com")]
   2: public class ContextItem
   3: {
   4:     private object value = null;
   5:     [DataMember]
   6:     public string Key { get; private set; }
   7:     [DataMember]
   8:     public object Value
   9:     {
  10:         get
  11:         {
  12:             return this.value;
  13:         }
  14:         set
  15:         {
  16:             if (this.ReadOnly)
  17:             {
  18:                 throw new InvalidOperationException("Cannot change the value of readonly context item.");
  19:             }
  20:             this.value = value;
  21:         }
  22:     }
  23:     [DataMember]
  24:     public bool ReadOnly { get; set; }
  25:     public ContextItem(string key, object value)
  26:     {
  27:         if (string.IsNullOrEmpty(key))
  28:         {
  29:             throw new ArgumentNullException("key");
  30:         }
  31:         this.Key = key;
  32:         this.Value = value;
  33:     }
  34: }

為了演示序列化和反序列化,我寫了如下兩個靜態的幫助方法。Serialize和Deserialize分別用於序列化和反序列化,前者將對象序列成成XML並保存到指定的文件中,後者則從文件讀取XML並反序列化成相應的對象。

   1: public static T Deserialize<T>(string fileName)
   2: {
   3:     DataContractSerializer serializer = new DataContractSerializer(typeof(T));
   4:     using (XmlReader reader = new XmlTextReader(fileName))
   5:     {
   6:         return (T)serializer.ReadObject(reader);
   7:     }
   8: }
   9:  
  10: public static void Serialize<T>(T instance, string fileName)
  11: {
  12:     DataContractSerializer serializer = new DataContractSerializer(typeof(T));
  13:     using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
  14:     {
  15:         serializer.WriteObject(writer, instance);
  16:     } 
  17:     Process.Start(fileName);
  18: }

我們的程序很簡單。從如下的代碼片斷中,我們先創建一個ContextItem對象,然後將ReadOnly屬性設置成true。然後調用Serialize方法將對象序列化成XML並保存在一個名稱為context.xml的文件中。然後調用Deserialize方法,讀取該文件進行反序列化。

   1: static void Main(string[] args)
   2: {
   3:     var contextItem1 = new ContextItem("__userId", "Foo");
   4:     contextItem1.ReadOnly = true;
   5:     Serialize<ContextItem>(contextItem1, "context.xml");
   6:     var contextItem2 = Deserialize<ContextItem>("context.xml");           
   7: }

序列化操作能夠正常執行,但當程序執行到Deserialize的時候拋出如下一個InvalidOperationException異常。

image

二、問題分析

從上麵給出的截圖,我們不難看出,異常是在給ContextItem對象的Value屬性賦值的時候拋出的。如果對DataContractSerializer序列化器的序列化/反序列化規則的有所了解的話,應該知道:對於數據契約(DataContract)基於屬性(Property)的數據成員(DataMember),序列器在反序列化的時候是通過調用Set方法對其進行初始化的。在本例中,由於ReadOnly是True,在對Value進行反序列化的時候必然會調用Set方法。但是,隻讀的ContextItem卻不能對其賦值,所以異常拋出。

那麼,如何來解決這個問題呢?我最初的想法是這樣:在序列化的時候將ReadOnly屬性設置成False,然後添加另一個屬性專門用於保存真實的值。在進行反序列的時候,由於ReadOnly為false,所以不會出現異常。當反序列化完成之後,在將ReadOnly的初始值賦上。雖然上述的方案能夠解決問題,但是為此對ContextItem添加一個隻在序列化和反序列化的過程中在有用的屬性,總覺得很醜陋。

我們不妨換一種思路:異常產生於對Value屬性凡序列化時發現ReadOnly非True的情況。那麼怎樣采用避免這種情況的發生呢?如果Value屬性先於ReadOnly屬性被序列化,那麼ReadOnly的初始值就是False,這個問題不就解決了嗎?這就是我們的第一個解決方案。

三、解決方案一:通過控製屬性反序列化順序

那麼,如果控製那麼屬性先被反序列化,那麼後被序列化呢?這就是要了解DataContractSerializer序列化器的序列化和發序列化規則了。在默認的情況下,DataContractSerializer是按照數據成員的名稱的順序進行序列化的。這可以從生成出來的XML的結構看出來。而XML元素的先後順序決定了反序列化的順序。

   1: <ContextItem xmlns:i="https://www.w3.org/2001/XMLSchema-instance" xmlns="https://www.artech.com">
   2:     <Key>__userId</Key>
   3:     <ReadOnly>true</ReadOnly>
   4:     <Value xmlns:d2p1="https://www.w3.org/2001/XMLSchema" i:type="d2p1:string">Foo</Value>
   5: </ContextItem>

在上麵的例子中,ContextItem的ReadOnly排在Value的前麵,會先被序列化。那麼,是不是我們要更新Value或者ReadOnly的數據成員(DataMember,不是屬性名稱)呢?這肯定不是我們想要的解決方案。在SOA的世界中,DataMember是契約的一部分,往往是不容許更改的。

如果在不更改數據成員名稱的前提下讓屬性Value先於ReadOnly被序列化,需要用到DataContractSerializer另一條反序列化規則:我們可以通過DataMemberAttribute特性的Order屬性控製序列化後的屬性在XML元素列表中的位置。

為此,我們有了答案,我們隻需要將ContextItem稍加改動就可以了。在如下的代碼中,在為Value和ReadOnly兩個屬性應用DataMemberAttribute的時候,將Order屬性分別設置成1和2,這樣就能使ContextItem對象在被序列化的時候,Value和ReadOnly屬性對應的XML元素將永遠會有前後之分。這裏還需要注意的是,在Value屬性的Set方法中,判斷是否隻讀,采用的不是ReadOnly屬性,而是對應的readonly字段。這一點非常重要,如果調用ReadOnly屬性將會迫使該屬性被反序列化。

   1: [DataContract(Namespace = "https://www.artech.com")]
   2: public class ContextItem
   3: {
   4:     private object value = null;
   5:     private bool readOnly;
   6:     [DataMember]
   7:     public string Key { get; private set; }
   8:  
   9:     [DataMember(Order = 1)]
  10:     public object Value
  11:     {
  12:         get
  13:         {
  14:             return this.value;
  15:         }
  16:         set
  17:         {
  18:             if (this.readOnly)
  19:             {
  20:                 throw new InvalidOperationException("Cannot change the value of readonly context item.");
  21:             }
  22:             this.value = value;
  23:         }
  24:     }
  25:     [DataMember(Order =2)]
  26:     public bool ReadOnly
  27:     {
  28:         get
  29:         {
  30:             return readOnly;
  31:         }
  32:         set
  33:         {
  34:             readOnly = value;
  35:         }
  36:     }
  37:     //Others
  38: }

有興趣的讀者可以親自試試看,如果我們進行了如上的更改,前麵的程序就能正常運行了。到這裏,有的讀者可以要問了,你不是說僅僅有一行代碼的變化嗎,我看上麵改動的不止一行嘛。沒有錯,我們完全可以作更少的更改來解決問題。

我們再換一種思維,之所以出現異常是在反序列化的時候調用Value屬性的Set方法所致。如果在反序列化的時候不調用這個方法不就得了嗎?那麼,如何才能避免對Value屬性的Set方法的調用呢?方法很簡單,那就是將數據成員定義在字段上,而不是屬性上。基於屬性的數據成員在反序列化的時候不得不通過調用Set方法對數據項進行初始化,而基於字段的數據成員在反序列化的時候隻需要直接對其複製就可以了。

基於這樣的思路,我們對原來的ContextItem進行簡單的改動——將DataMemberAttribute特性從Value屬性移到value字段上。需要注意的,為了符合於原來的Schema,需要將DataMemberAttribute特性的Name屬性設置成“Value”。

   1: [DataContract(Namespace = "https://www.artech.com")]
   2: public class ContextItem
   3: {
   4:     [DataMember]
   5:     public string Key { get; private set; }
   6:  
   7:     [DataMember(Name = "Value")]
   8:     private object value = null;
   9:     public object Value
  10:     {
  11:         get
  12:         {
  13:             return this.value;
  14:         }
  15:         set
  16:         {
  17:             if (this.ReadOnly)
  18:             {
  19:                 throw new InvalidOperationException("Cannot change the value of readonly context item.");
  20:             }
  21:             this.value = value;
  22:         }
  23:     }
  24:     [DataMember]
  25:     public bool ReadOnly { get; set; }     
  26:      //Others
  27:     }
  28: }

總結

雖然這僅僅是一個很小的問題,解決的方案看起來也是如此的簡單。但是,這並不意味著這是一個可以被忽視的問題,背後隱藏對DataMemberAttribute序列化的序列化規則的理解。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 14:34:09

  上一篇:go  隻在UnitTest和WebHost中的出現的關於LogicalCallContext的嚴重問題
  下一篇:go  -如何搭建聚合支付係統