.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

深入挖掘.NET序列化机制——实现更易用的序列化方案的更多相关文章

  1. Hadoop阅读笔记(六)——洞悉Hadoop序列化机制Writable

    酒,是个好东西,前提要适量.今天参加了公司的年会,主题就是吃.喝.吹,除了那些天生话唠外,大部分人需要加点酒来作催化剂,让一个平时沉默寡言的码农也能成为一个喷子!在大家推杯换盏之际,难免一些画面浮现脑 ...

  2. Hadoop序列化机制及实例

    序列化 1.什么是序列化?将结构化对象转换成字节流以便于进行网络传输或写入持久存储的过程.2.什么是反序列化?将字节流转换为一系列结构化对象的过程.序列化用途: 1.作为一种持久化格式. 2.作为一种 ...

  3. 由浅入深了解Thrift之服务模型和序列化机制

    一.Thrift介绍 Thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发.它结合了功能强大的软件堆栈和代码生成引擎.其允许你定义一个简单的定义文件中的数据类型和服务接口.以作为输入文件,编 ...

  4. Java I/O系统学习系列五:Java序列化机制

    在Java的世界里,创建好对象之后,只要需要,对象是可以长驻内存,但是在程序终止时,所有对象还是会被销毁.这其实很合理,但是即使合理也不一定能满足所有场景,仍然存在着一些情况,需要能够在程序不运行的情 ...

  5. Zookeeper 序列化机制

    一.到底在哪些地方需要使用序列化技术呢? 二.Zookeeper(分布式协调服务组件+存储系统) Java 序列化机制 Hadoop序列化机制 Zookeeper序列化机制 一.到底在哪些地方需要使用 ...

  6. C++ folly库解读(三)Synchronized —— 比std::lock_guard/std::unique_lock更易用、功能更强大的同步机制

    目录 传统同步方案的缺点 folly/Synchronized.h 简单使用 Synchronized的模板参数 withLock()/withRLock()/withWLock() -- 更易用的加 ...

  7. Thrift 个人实战--Thrift 的序列化机制

    前言: Thrift作为Facebook开源的RPC框架, 通过IDL中间语言, 并借助代码生成引擎生成各种主流语言的rpc框架服务端/客户端代码. 不过Thrift的实现, 简单使用离实际生产环境还 ...

  8. hrift 的序列化机制

    Thrift 个人实战--Thrift 的序列化机制 前言: Thrift作为Facebook开源的RPC框架, 通过IDL中间语言, 并借助代码生成引擎生成各种主流语言的rpc框架服务端/客户端代码 ...

  9. Java序列化机制和原理及自己的理解

    Java序列化算法透析 Serialization(序列化)是一种将对象以一连串的字节描述的过程:反序列化deserialization是一种将这些字节重建成一个对象的过程.Java序列化API提供一 ...

随机推荐

  1. 图文介绍如何在Eclipse统计代码行数(转)

    使用Eclipse可以方便的统计工程或文件的代码行数,方法如下: 1.点击要统计的项目或许文件夹,在菜单栏点击Search,然后点击File... 2.选中正则表达式(Regular expressi ...

  2. fat32转ntfs

    convert c: /fs:ntfs 下了个维基的zim,7G,fat32放不下 :( Microsoft Windows [版本 6.1.7600] 版权所有 (c) 2009 Microsoft ...

  3. windows 查看软件是32位还是64位

    我有一个配置挺好的电脑,win10 64位的系统,但是最近下载的一个软件用着巨慢,导致我严重想知道下载的软件是64位的还是32位的 百度谷歌了很久,大多数都说是两个方法: 1. 判断文件的安装路径,如 ...

  4. POJ 1066 Treasure Hunt (线段相交)

    题意:给你一个100*100的正方形,再给你n条线(墙),保证线段一定在正方形内且端点在正方形边界(外墙),最后给你一个正方形内的点(保证不再墙上) 告诉你墙之间(包括外墙)围成了一些小房间,在小房间 ...

  5. cant create oci environment

    网上这些人真是七里八里呀,下了navicat premium,想连接远程数据库,结果报cant create oci environment. 看了好几篇帖子博客,都说要下一个instantclien ...

  6. 蜻蜓FM笔试题目,求两个点的最近父节点

    这个博客写的特别好. http://blog.csdn.net/kangroger/article/details/40392925

  7. XV Open Cup named after E.V. Pankratiev. GP of Tatarstan

    A. Survival Route 留坑. B. Dispersed parentheses $f[i][j][k]$表示长度为$i$,未匹配的左括号数为$j$,最多的未匹配左括号数为$k$的方案数. ...

  8. 【HDU2222】Keywords Search AC自动机

    [HDU2222]Keywords Search Problem Description In the modern time, Search engine came into the life of ...

  9. CODEVS1090 加分二叉树

    codevs1090 加分二叉树 2003年NOIP全国联赛提高组 题目描述 Description 设一个n个节点的二叉树tree的中序遍历为(l,2,3,…,n),其中数字1,2,3,…,n为节点 ...

  10. BZOJ 2048 题解

    2048: [2009国家集训队]书堆 Time Limit: 10 Sec  Memory Limit: 259 MBSubmit: 1076  Solved: 499[Submit][Status ...