天天看點

深入挖掘.NET序列化機制——實作更易用的序列化方案

.NET架構為程式員提供了“序列化和反序列化”這一有力的工具,使用它,我們能很容易的将記憶體中的對象圖轉化為位元組流,并在需要的時候再将其恢複。這一技術的典型應用場景包括[1] :

  • 應用程式運作狀态的持久化;
  • 在應用程式之間通過剪切闆傳送對象;
  • 建立對象複本,以隔離使用者操作造成的影響;
  • 在網絡間傳送對象。

然而,.NET架構提供的預設序列化行為也存在着有諸多限制,尤其是在版本控制方面——比如一個使用SerializableAttribute标記,而未實作ISerializable的類型,在通過重構修改了某個字段的名稱後,再反序列化之前的序列化結果時就會失敗。

本文首先舉例說明了.NET預設序列化方案的限制;然後描述了通過擴充.NET序列化架構而期望達到的目标——我們已經在實際開發中實作;接下來介紹了.NET序列化架構所提供的擴充點,最後詳細說明了如何通過這些擴充點實作一個更易用的序列化方案。

需要特别說明的兩點是:

  1. 完整的.NET序列化架構是非常強大的,而我們的改進方案仍然處于這一架構之内。本文所說的”局限”隻是指使用SerializableAttribute、ISerializable時的局限,并不是整個序列化架構的局限。如果你已經對ISurrogateSelector、ISerializationSurrogate、SerializationBinder等概念很熟悉了,我想你也許已經有了自己的解決方案。
  2. 為什麼要在.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的實作,至于此類的其它功能則不再贅述。

這樣我們就實作了對任意類型進行自定義序列化的功能,下面簡要總結一下:

  1. 為要序列化的類型T定義一個IPersistor接口的實作P;
  2. 将T與P注冊到PersistorManager中;
  3. 在建立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的實作邏輯如下:

  1. 提取mapped上所有有PersistedAttributed标記的成員;
  2. 在序列化時(IPersistor::GetObjectData中),疊代所有的标記項,用标記的名稱和具體的字段值調用info.AddValue;
  3. 在反序列化時(IPersistor::SetObjectData中),疊代所有标記項,用标記的名稱從SerializationInfo中取值,并作如下處理:
    1. 如果取值成功,則将其設定給要反序列化的對象;
    2. 如果擷取失敗,則
      1. 如果此項未标記為Optional,則抛出異常,否則:
      2. 如果此項有Value或Calculator設定,則通過它們為目前字段指派,否則保留字段的預設值(default(T))。

至于具體實作代碼,我們這裡就省略了。 

省略掉的部分

讀到這裡,相信大家對.NET的序列化架構的擴充就有所感受了。不過我們還沒有介紹如何處理類型名稱改變的問題,這裡隻給出一個引子——使用BinaryFormatter的Binder屬性和自定義的SerializationBinder派生類——更多的細節相信大家都能搞定的。

其實.NET序列化機制還有很多可以挖掘的地方,比如IObjectReference,每一個看似簡單的接口都能給我們無限發揮的空間。

好了,就到這裡吧。歡迎大家來探讨。

參考資料:

[1] http://msdn.microsoft.com/en-us/magazine/cc301761.aspx

繼續閱讀