您的位置:首页 > 其它

WCF学习笔记——对象序列化

2013-11-08 13:43 288 查看
当试图通过Web服务、WCF这样的远程处理技术将一个对象复制到远端时,具有对类型序列化的能力很关键。

一 序列化基础

  序列化描述了持久化或传输一个对象的状态到流的过程(.NET将对象序列化到流,流是字节的逻辑序列,与特定的介质无关)。被持久化的数据次序包括所有以后需要用来重建(反序列化)对象所需要的信息。通过序列化,我们用最小的花费来保存或传输海量的数据。使用[Serializable]序列化对象非常简单,但幕后的调用过程十分复杂。当一个对象被序列化时,其集成连上的所有数据都会被包括进来,一组相关的对象使用对象图来表现。.NET允许使用多种格式来保存一个对象图,包括二进制、SOAP和XML,对象图可以持久化为任意的System.IO.Stream派生类。

(一) 对象图

  当对象被序列化时,CLR将处理所有相关的对象,一组关联对象被总称为一个对象图。它提供了一种很简明的方式来记载一组对象的引用关系。对象图中的每个对象被赋予一个独有的数值类型的值,这个值可任意地赋给对象图中的成员,以此来记录对象的依赖关系。



  上图可知,Order引用了Product,OnlineOrder引用了Order和Product。在CLR中,这种关系表示为:[Order 3,ref 2],[Product 2],[OnlineOrder 1,ref 3,ref 2]。当序列化OnlineOrder时,根据对象图,使Order和Product参与其中。(注意XmlSerializer不使用对象图。)

(二) 使用序列化

  .NET通过反射自动实现了对象的序列化和反序列化。.NET能够捕获对象的每个字段的值,并将其序列化。对于反序列化,.NET使用反射创建一个对应类型的新对象,读取它的持久化字段值,然后设置字段值。由于反射可以打破封装,所以.NET在序列化期间能够完整地反映出对象的状态,从而保证了反序列化可以完整地重建对象状态。通过为类型添加[Serializable]将默认类型是所有数据成员参与序列化,使用[NonSerialized]可以指定不需要序列化的成员。当.NET序列化成员时,首先通过反射确定它是否具有[NonSerialized]特性,如果有就会忽略该字段,直接跳过他。[Serializable]不能被继承,使用必须在继承了链的每个类型上进行声明。实际上,对象图中的所有对象必须被标记为[Serializable]。(因为XmlSerializer不使用对象图,所以不需要对类型标记[Serializable]。)

  System.Runtime.Serialization.IFormater定义了核心的Serialize()和Deserialize()方法,Serialize()和Deserialize()将完成对象图和流之间的转换。.NET为类型的序列化和反序列化提供了两种格式器,BinaryFormatter会将其序列化为二进制格式,SoapFormatter则使用.NET特定的SOAP XML格式。它们都实现了IFormater接口。除了持久化对象的状态,这两种格式器都会将类型的程序集以及版本控制信息持久化到流中,以保证反序列化为正确类型。

  System.Runtime.Remoting.Messaging.IRemotingFormatter派生于IFormater,它重载了Serialize()和Deserialize()成员使风格更适合于分布式持久化。

  不同的序列化器对类型保真(type fidelity)的支持程度不同,其反应在对私有字段的序列化以及对程序集信息的序列化上。如果希望序列化的对象可以被.NET之外的其它环境使用,XmlSerializer是理想的选择。(Web Service默认使用XmlSerializer)

1 二进制

  BinaryFormatter类型使用紧凑的二进制格式将对象图序列化为一个流,这个类型在System.Runtime.Serialization.Formatters.Binary命名空间中定义。

  使用BinaryFormatter时,不仅仅将对象图中对象的字段数据进行序列化(所有未标记[NonSerialized]的字段,包括公有的和私有的),还将序列化每个对象类型的完全限定名和定义程序集的完整名称(名称、版本、公钥、区域性)。这些数据使BinaryFormatter在希望用值(Marshaling By Value,按值编组)跨越.NET应用程序机器边界传递对象时成为理想的选择。



  可以使用下列方式干预BinaryFormatter的序列化过程:

为字段标记[NonSerialized]。

实现ISerializable接口自定义序列化过程。

实现IDeserializationCallback,为对在完成对象图序列化时所调用的方法的部分支持。(系统在反序列化的时候会检查待类型是否实现了IDeserializationCallback接口,如果实现了,那么系统就调用该接口中的OnDeserialization方法。)

设置Binder属性。

2 SOAP

  SoapFormatter类型将对象序列化为一个SOAP消息,这个类型在System.Runtime.Serialization.Formatters.Soap命名空间中定义。

  SoapFormatter通过使用XML命名空间来序列化原始程序集的信息,所有未标记[NonSerialized]的字段,包括公有的和私有的都会被序列化,无法序列化泛型,可以序列化数组。



  可以使用下列方式干预SoapFormatter的序列化过程:

使用相关特性,如SoapIgnoreAttribute。

实现IDeserialilzationCallback。

设置Binder属性。

3 XML

  XmlSerializer类型将对象序列化为XML,这个类型在System.Xml.Serialization命名空间中定义。

  XmlSerializer不具备类型保真(原因是XML数据表现形式具有终端开放性),因此也不记录类型完全限定名称或起源的程序集,只有公有字段或拥有公有属性的私有字段才会被序列化,为通过属性公开的私有字段会被忽略。



  XmlSerializer在默认的情况下具有如下的序列化和反序列化规则:

XML根节点的名称为对象类型的名称,并且没有命名空间。

对象属性或字段成员以XmlElement的形式输出,名称和属性/字段名称一致,并且不具有命名空间。

只有public属性/字段成员才会参与序列化。

只有可读/写的属性才能被序列化。

XmlElement的顺序与对应的属性或字段在类型中定义的顺序一致。

无参构造器是必须的,因为反序列化的时候需要调用它创建对象。

  可以使用下列方式干预XmlSerializer的序列化过程:

使用相关特性,如XmlIgnoreAttribute。

实现IXmlSerializable。

实现IDeserialilzationCallback。

用XmlTypeMapping或XmlAttributeOverrides构造XmlSerializer。

二 WCF序列化

  原有的格式化器需要将类型的程序集及版本控制信息持久化到流中,以保证对象被反序列化为正确的类型,这种方式一定程度上妨碍面向服务交互。原因如下:BinaryFormatter和SoapFormatter具备类型保真,它们要求其所在程序集、版本等信息必须完全一致,如果不同则视为是不同类型的对象。面向服务的交互方式应该更倾向于平台无关,并最大限度的解耦,因此这2个格式化器显然与WCF有些不搭。再看XmlSerializer格式化器,在功能上与DataContractSerializer格式器类似,但是为何没有使用XmlSerializer格式器而是引人新格式器,我的理解如下:在SOA服务的构建过程中,提倡契约优先的构建原则,也就是说我们应该优先构建契约后构建具体的代码,在构建过程中并不考虑其具体的序列化过程。(契约优先相对于代码优先在设计层面具有更高的抽象程度,降低了服务设计与平台的耦合度,因此我更倾向于契约优先的开发顺序。)此外,XmlSerializer为如何将数据表示成XML提供了精确的控制,DataContractSerializer则提供了较少的控制,所以DataContractSerializer的序列化过程是可预知的,容易优化,所以它与XmlSerializer相比有更好的性能(我查到的数字是提升了约10%)。

  DataContractSerializer通过WriteObject和ReadObject方法完成序列化和反序列化。DataContractSerializer具有如下5个重要属性的成员,以及多个构造器重载来指定它们:

DataContractSurrogate 返回一个数据契约代理类的对象,数据契约代理实现了IDataContractSurrogate,可以用于干预DataContractSerializer的序列化、反序列化,以及契约的导入和导出行为。

IgnoreExtensionDataObject 是否忽略扩展数据对象。

KnownTypes 已知类型集合,包含了在序列化和反序列化时不能被解析的类型。

MaxItemsInObjectGraph 进行序列化和反序列化允许的最大对象数,默认为65536。

PreserveObjectReferences 表示如果数据对象的多个属性或字段引用相同的对象,在序列化的时候是否要在XML中保持一样的引用结构。

  WCF格式器DataContractSerializer并没有实现IFormater,而是继承自XmlObjectSerializer,在使用其实现类型与流之间的序列化操作时,代码与BinaryFormatter和SoapFormatter的写法略微不同,需要调用带参数的构造函数来提供操作的类型,其代码形如:

DataContractSerializer dcs = new DataContractSerializer(typeof(Product));
using (Stream fs = new FileStream("test.soap", FileMode.Create, FileAccess.Write, FileShare.None))
{
dcs.WriteObject(fs, product);//反序列化使用ReadObject方法,但返回的是Object类型,需要进行显示类型转换。
}


  序列化结果如下:



  DataContractSerializer在默认的情况下具有如下的序列化规则:

XML的根节点名称为数据契约类型的名称,默认的命名空间为http://schemas.datacontract.org/2004/07/{数据契约类型的命名空间}。

只有显式地应用了[DataMember]的字段或属性才能作为数据成员参与序列化。

所有数据成员均以XML元素的形式被序列化。

序列化后数据成员在XML中的次序为:父数据成员在前,子类数据成员在后,定义在同一个类型中的数据成员按照字母排序。

  限定序列化对象的数量

  MaxItemsInObjectGraph属性表示允许被序列化或反序列化对象的数量上限。一旦超过该值,序列化或反序列化会立即终止并抛出异常,从而在一定程度上缓解了通过发送大集合数据形式的拒绝服务攻击的风险。其计算规则为:对象自身算一个对象,使用成员及所有内嵌的成员都算一个对象。

  保持对象现有的引用结构

  PreserveObjectReferences属性表示是否在序列化的时候保持相应的引用结构。

  此外,WCF还定义了NetDataContractSerializer格式器,它实现了IFormater接口(为了兼容旧的序列化模式),所以它和旧的格式器相似,可以序列化类型信息,其使用方法也与传统序列化器类似,其代码形如:

IFormatter formatter = new NetDataContractSerializer();
using (Stream fs = new FileStream("test.soap", FileMode.Create, FileAccess.Write, FileShare.None))
{
formatter.Serialize(fs, product);
}


  序列化结果如下:



  当类型同时使用[DataContract]和[Serializable]修饰时,NetDataContractSerializer以[DataContract]的设置为准。使用NetDataContractSerializer序列化的对象,可以使用DataContractSerializer反序列化,但反之不行。

三 数据契约与旧序列化模式

[Serializable]和[DataContract]可以同时使用。

[Serializable]指明类型的所有成员均参与序列化,要求开发者显示指定不参与序列化的成员;而[DataContract]以明确参与的方式,要求开发者显示指定参与序列化的成员。

[Serializable]不支持别名,也无法将一个新类型映射为预定义的数据契约;而[DataContract]支持别名,并且可以指定成员顺序。

[Serializable]会隐式序列化未标记[NonSerialized]的私有字段,破坏属性的封装性;而[DataContract]只会序列化指定的成员,即使成员是私有的。

[Serializable]没有直接支持版本控制,因为它获取了程序集信息,不容易进行控制;而[DataContract]易于版本控制。

四 新旧序列化的选择

  选择旧序列化的目的在于创建更为复杂的对象。如果使用WCF默认的格式器,我们必须为成员同时提供getter、setter方法,且其为了满足SOA的需要,传递的内容无异于XML数据。这类对象通常被称为DTO(Data Transfer Object,数据传输对象)。这是一种简单的对象,不包含业务逻辑,不与数据以外的类型关联,可以理解为简单的数据容器。该容器是非智能的、不受保护的、不安全的,我们无法限制客户端局部初始化一个对象或者往容器中注入无效数据。

  所以当我们需要为跨平台而使用DTO时,我们应该选择数据契约;而当我们想要创建复杂的传输对象时,[Serializable]显然更合适,它不仅会序列化对象的数据也会序列化任何处理事件的对象(使用时要注意,可能会发生你预料之外的序列化错误),但是你将失去跨平台的特性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: