.NET框架為程序員提供了“序列化和反序列化”這一有力的工具,使用它,我們能很容易的將內存中的對象圖轉化為字節流,並在需要的時候再將其恢復。這一技術的典型應用場景包括[1] :
- 應用程序運行狀態的持久化;
- 在應用程序之間通過剪切板傳送對象;
- 創建對象復本,以隔離用戶操作造成的影響;
- 在網絡間傳送對象。
然而,.NET框架提供的默認序列化行為也存在着有諸多限制,尤其是在版本控制方面——比如一個使用SerializableAttribute標記,而未實現ISerializable的類型,在通過重構修改了某個字段的名稱后,再反序列化之前的序列化結果時就會失敗。
本文首先舉例說明了.NET默認序列化方案的限制;然后描述了通過擴展.NET序列化框架而期望達到的目標——我們已經在實際開發中實現;接下來介紹了.NET序列化框架所提供的擴展點,最后詳細說明了如何通過這些擴展點實現一個更易用的序列化方案。
需要特別說明的兩點是:
- 完整的.NET序列化框架是非常強大的,而我們的改進方案仍然處於這一框架之內。本文所說的”局限”只是指使用SerializableAttribute、ISerializable時的局限,並不是整個序列化框架的局限。如果你已經對ISurrogateSelector、ISerializationSurrogate、SerializationBinder等概念很熟悉了,我想你也許已經有了自己的解決方案。
- 為什么要在.NET序列化框架內進行擴展,而不是實現另外一套獨立的解決方案呢?一來我們想充分利用框架提供的機制,比如對象圖的訪問與維護——這些邏輯實現起來應該會比較困難;再者可以充分利用已有的序列化代碼,比如基礎類庫中的類型和第三方庫中的類型——它們大都還是使用的.NET默認序列化機制,這樣可以節省非常大的工作量。
.NET默認序列化方案及其的限制
在.NET中,為了使某個類型成為可序列化的,最簡單的方式是使用SerializableAttribute屬性,如下代碼所示。
[Serializable] class Person { private string name; private int agee; ... } var formatter = new BinaryFormatter(); formatter.Serialize(someStream, aPerson);
但在實際的開發項目中,如果真的這樣做了,那么在產品的第一個版本發布后,就很有可能會面臨下面的問題。
- 你發現agee拼寫錯了,非常想改正它(嗯,我是完美主義者)!
- 你發現Person這個名字太寬泛了,也許改為Employer更好。
- 你想添加一個新字段,但又不想或不能在IDeserializationCallback中為新字段賦值——確實存在這種情況,請參考我的另一篇文章http://www.cnblogs.com/brucebi/archive/2013/04/01/2993968.html。
- 你想在Person的序列化過程中獲得更多的控制,所以想改為實現ISerializable接口。
所有這些都不能實現,因為它們所帶來的代碼修改都將造成軟件不再兼容之前的版本。是的,我們總可以選擇在最開始的時候就使用ISerializable接口(和一個反序列化構造函數),但如果你也有”懶惰“的美德,就會覺得這樣做很不爽!
此外,我們在開發過程中還遇到了如下幾種情況:
- 序列化第三方庫中的對象,但這些類型沒有標記為Serializable。
- 有時我們需要把某個屬性(property)序列化,而非相應的字段,這樣反序列化時就可以通過屬性的set方法執行一些額外的邏輯。
- 某些對象的序列化過程效率很低,我們想提供一個更高效的實現。
- 我們想實現一個”對象/存儲映射“方案,以使我們能像”對象/關系映射“那樣在內存與存儲設備(包括數據庫、文件系統等)間進行轉儲。我們的方案不像ORM那樣復雜,但它更適合我們的產品,效率更高,自動化程度更高。
而要解決這些問題,都要訴諸於對.NET序列化框架更深入的理解。
我們能做到什么
在最終的方案中我們將給出一個PersistedAttribute和一個IPersistable接口,它們與SerializableAttribute和ISerializable很類似,但能解決前面提到的問題。下面的代碼說明了PersistedAttribute的使用方法及其功能。
// 使用PersistedAttribute代替SerializableAttribute,以使用自定義的序 // 列化機制進行處理。類的名字可以修改,只要保證Persisted的參數不變就 // 可以。 [Persisted("a unique identifier of the class")] class Person { // 與SerializableAttribute不同,只有明確標記為Persisted的字段 // 或屬性才會被序列化。而且每個序列化項都可以指定一個名稱,這 // 樣,當字段名稱改變后,只要此項的名稱不變,就能兼容之前的版 // 本。比如,可以把name改為fullName,而無須做其它任何修改。 [Persisted("Name")] private string name; [Persisted("Age")] private int age; // 對於新添加的字段,通過將其聲明為“可選的”,反序列化過程就 // 不會出錯,之后可以在IDeserializationCallback中為其賦值,或 // 者可以通過Value屬性為其指定默認值。 [Persisted("Id", Optional = true, Value = "any thing")] private string id; // 對於新添加的可選項,也可以為其指定一個計算函數,這樣系統在 // 反序列化時如果發現流中沒有存儲相應的值,就會調用此函數來為 // 其計算相應的值。 [Persisted("Gender", Optional = true, Calculator = CalculateGenderById)] private Gender gender; // 屬性也可以被序列化。在序列化時,系統將保存get方法的結果,而 // 反序列化時,則會通過set方法設置屬性的值,此方法中的所有邏輯 // 都將會執行。 [Persisted("SomeProperty")] public int SomeProperty { get { ... } set { ... } } }
IPersistable接口則可以與PersistedAttribute進行各種組合、替換,如下面的代碼所示。
public interface IPersistable { // 在序列化時獲取對象的數據。 void GetObjectData(SerializationInfo info); // 在反序列化時設置對象的數據。 void SetObjectData(SerializationInfo info); } // 與ISerializable一樣,實現IPersistable接口也要求有Persisted標記。 [Persisted("a unique identifier of the class")] class Person : IPersistable { // Persisted標記與IPersistable接口可以共存,系統會先處理 // 被標記的字段或屬性,然后調用IPersistable接口的成員。 [Persisted("Name")] private string name; // 可以去掉之前使用的Persisted標記,然后在IPersistable // 接口中以同樣的名稱進行讀取或設置。 // [Persisted("Age")] private int age; private Gender gender; void GetObjectData(SerializationInfo info) { info.SetValue("Age", age); // 保存額外的數據。 info.SetValue("G", gender); } void SetObjectData(SerializationInfo info) { // 讀取之前使用標記序列化的內容。 age = info.GetInt32("Age"); try { // 處理版本兼容問題。 gender = (Gender)info.GetValue("G"); } catch (Exception e) { } } }
此外,新的方案還提供了IPersistor接口,通過它可以為任何類型提供自定義的序列化代碼,不管這個類型是不是可序列化的。
// 可以為其它類型的對象提供序列化功能的接口。 public interface IPersistor { void GetObjectData(object obj, SerializationInfo info); void SetObjectData(object obj, SerializationInfo info); } // 為List<int>類型的對象提供自定義的序列化代碼,雖然List<int>本身已經是可序列化的. public class IntListPersistor : IPersistor { void GetObjectData(object obj, SerializationInfo info) { var list = (List<int>)obj; // some more efficient codes. ... ... info.SetValue(...); } void SetObjectData(object obj, SerializationInfo info) { var list = (List<int>)obj; // some more efficient codes. ... ... someValue = info.GetValue(...); } } // 可以為其它非可序列化類型提供自定義的序列化代碼。 public class RelativePersistor : IPersistor { void GetObjectData(object obj, SerializationInfo info) { var target = (Some3rdPartyNonSerializableClass)obj; ... ... } void SetObjectData(object obj, SerializationInfo info) { var target = (Some3rdPartyNonSerializableClass)obj; ... ... } } // 需要在程序開始的時候將實現類注冊到系統中。 PersistManager.Register(typeof(List<int>), new IntListPersistor()); PersistManager.Register(typeof(Some3rdPartyNonSerializableClass, new RelativePersistor());
下面,我們將介紹相應技術的實現思路。
如何切入.NET的序列化框架
首先要解決的問題是:如何將自定義的序列化機制插入到.NET序列化框架中。我假設你已經知道如何使用BinaryFormatter或者SoapFormatter,而在此我想簡單的描述一下formatter的一些行為細節。后文中我們將以BinaryFormatter為例。
BinaryFormatter上有一個SurrogateSelector屬性(surrogate是“代理”的意思,surrogate selector便是代理選擇器),它的類型如下代碼所示:
public interface ISurrogateSelector { void ChainSelector(ISurrogateSelector selector); ISurrogateSelector GetNextSelector(); ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector); }
其中用到的ISerializationSurrogate(serialization surrogate便是序列化代理嘍)的定義如下:
public interface ISerializationSurrogate { void GetObjectData(Object obj, SerializationInfo info, StreamingContext context); Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector); }
當BinaryFormatter在序列化一個對象obj時,它會檢查自己的SurrogateSelector屬性是否非空,如果非空,便會以obj的類型為參數調用其GetSurrogate方法,如果此方法返回一個有效的對象surrogate(ISerializationSurrogate),則formatter會調用surrogate.GetObjectData(obj, ...),這時surrogate對象便獲得機會來執行自定義的邏輯了。
對,這就是奇跡發生的地方!
我們要做的就是實現自定義的ISurrogateSelector和ISerializationSurrogate類,在合適的時候調用自定義的代碼。然后,在使用時將其注入到BinaryFormatter中,如下面代碼所示。
var formatter = new BinaryFormatter( new TheSurrogateSelector, new StreamingContext(StreamingContextStates.All)); formatter.Serialize(stream, objectGraph);
實現更易用的序列化方案
先來看ISurrogateSelector的實現(為簡潔起見,去掉了很多優化相關的代碼)。
public class PersistSurrogateSelector : SurrogateSelector { private readonly PersistSurrogate inner = new PersistSurrogate(); private readonly ISerializationSurrogate surrogate; public PersistSurrogateSelector() { surrogate = FormatterServices.GetSurrogateForCyclicalReference(inner); } public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector) { return inner.CanHandle(type) ? surrogate : base.GetSurrogate(type, context, out selector); } }
其中的PersistSurrogate即是我們自定義的序列化代理,其代碼如下所示:
public class PersistSurrogate : ISerializationSurrogate { public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) { // 使用注冊的IPersistor實現來對對象進行序列化和反序列化。 var manager = PersistorManager.Instance; var persistor = manager.GetPersistor(obj.GetType()); persistor.GetObjectData(obj, info); } public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { var manager = PersistorManager.Instance; var persistor = manager.GetPersistor(obj.GetType()); return persistor.SetObjectData(obj, info); } internal bool CanHandle(Type type) { // 如果已經為類型注冊了IPersistor接口實現,則自定義機制可以處理。 return PersistorManager.Instance.IsPersistable(type); } }
代碼中的PersistorManager就是管理自定義的IPersistor接口實現與相應的類型對應的管理類(前面的代碼中提到過),一會兒我們會提到PersistorManager.Instance.IsPersistable的實現,至於此類的其它功能則不再贅述。
這樣我們就實現了對任意類型進行自定義序列化的功能,下面簡要總結一下:
- 為要序列化的類型T定義一個IPersistor接口的實現P;
- 將T與P注冊到PersistorManager中;
- 在創建BinaryFormatter時,將PersistSurrogateSelector對象傳入;
有了這個基礎,再實現IPersistable和PersistedAttribute功能就比較簡單了,來看PersistorManager::IsPersistable方法的實現:
public bool IsPersistable(Type type) { Utility.CheckNotNull(type); var at = typeof(PersistedAttribute); return Attribute.GetCustomAttribute(type, at, false) != null || exactMatches.ContainsKey(type) || derivedMatches.GetValue(type) != null || dynamicMatches.Any(i => i.CanHandle(type)); }
其中的Attribute.GetCustormAttribute即是在判斷具體的類型上是否有PersistedAttribute標記,如果有我們就可以為其合成一個IPersistor接口的實現——這樣,使用PersistedAttribute標記的類型,則不再需要顯示的注冊。代碼中的exactMatches,derivedMatches和dynamicMatches分別用於處理那些能夠為某個具體的類型提供序列化功能,能夠為一個派生體系提供序列化功能及能夠動態決定是否可以提供序列化功能的IPersistor實現。
我們可以定義一個Persistor實現來統一處理那些有PersistedAttribute標記的類型,在此之前先來看PersistorManager::GetPersistor的定義:
public IPersistor GetPersistor(Type mapped) { Utility.CheckNotNull(mapped); if (exactMatches.ContainsKey(mapped)) { return exactMatches[mapped]; } var derived = derivedMatches.GetValue(mapped); if (derived != null) { exactMatches[mapped] = derived; return derived; } var dynamic = dynamicMatches.FirstOrDefault(i => i.CanHandle(mapped)); if (dynamic != null) { exactMatches[mapped] = dynamic; return dynamic; } var auto = new Persistor(mapped); exactMatches[mapped] = auto; return auto; }
而Persistor的實現邏輯如下:
- 提取mapped上所有有PersistedAttributed標記的成員;
- 在序列化時(IPersistor::GetObjectData中),迭代所有的標記項,用標記的名稱和具體的字段值調用info.AddValue;
- 在反序列化時(IPersistor::SetObjectData中),迭代所有標記項,用標記的名稱從SerializationInfo中取值,並作如下處理:
- 如果取值成功,則將其設置給要反序列化的對象;
- 如果獲取失敗,則
- 如果此項未標記為Optional,則拋出異常,否則:
- 如果此項有Value或Calculator設置,則通過它們為當前字段賦值,否則保留字段的默認值(default(T))。
至於具體實現代碼,我們這里就省略了。
省略掉的部分
讀到這里,相信大家對.NET的序列化框架的擴展就有所感受了。不過我們還沒有介紹如何處理類型名稱改變的問題,這里只給出一個引子——使用BinaryFormatter的Binder屬性和自定義的SerializationBinder派生類——更多的細節相信大家都能搞定的。
其實.NET序列化機制還有很多可以挖掘的地方,比如IObjectReference,每一個看似簡單的接口都能給我們無限發揮的空間。
好了,就到這里吧。歡迎大家來探討。
參考資料: