雁过请留痕...
代码改变世界

《CLR via C#》笔记——运行时序列化(3)

2012-07-18 20:54  xiashengwang  阅读(941)  评论(5编辑  收藏  举报

七,流上下文

  前面讲过,一组序列化好的对象可以有许多的目的地;同一进程,同一台机器的不同进程,不同机器的不同进程等。在一些比较少见的情况下,一个对象可能想知道他要在什么地方被反序列化,从而以不同的方式生成它的状态。例如:一个包装了Windows信号量(semaphore)的一个对象,如果它知道反序列化到同一个进程中,就可能决定序列化它的内核句柄(kernel handle),这是因为内核句柄在同一进程中有效。然而,如果它知道要反序列化到同一机器的不同进程中,就可能对信号量的字符串名称进行序列化。最后,如果对象知道它要反序列化到一台不同的机器上的一个进程中,就可能抛出异常,因为信号量值在同一台机器中有效。(.Net 4.0中信号量已经被标注为不能序列化了)

  本章提到的大量方法都接受一个StreamingContext,StreamingContext只有两个只读属性:

●State:StreamingContextState枚举,一组为标志,指定要序列化/反序列化的对象的来源或目的地。

●Context:object类型,对一个对象的引用,对象中包含用户希望的任何上下文信息。

StreamingContextState枚举值的含义:

●CrossProcess:来源或目的地在同一机器的不同进程。

●CrossMachines:来源或目的地在不同的机器上。

●File:来源或目的地是一个文件。不保证在同一个进程。

●Persistence:来源或目的地是一个存储(store),比如数据库或文件。不保证是同一个进程。

●Remoting:来源或目的地是远程的一个位置,可能是同一机器,也可能不是。

●Other:来源或目的地不明确。

●Clone:对象图被克隆。序列化代码认定是在同一进程对数据进行反序列化,可以安全的访问句柄或其他托管资源。

●CrossAppDomain:来源或目的地在不同的AppDomain。

●All:来源或目的地可能是上述的任意一个,这是默认设定。

知道了如何获取他们,接下来讨论如何设置它们。IFormatter接口(同时由BinaryFormatter和SoapFormatter实现)定义了一个StreamingContext类型的可读可写属性,名为Context。构造一个格式化器的时候可以初始化这个属性。如果不显示初始化,格式化器会初始化一个StreamingContextState为All,额外对象引用为null的StreamingContext。显示设定的代码如下:

        private Stream SerializeToMemory(object graph)
        {
            MemoryStream stream = new MemoryStream();
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Context = new StreamingContext(StreamingContextStates.Clone, null);
            formatter.Serialize(stream, graph);
            return stream;
        }

八,将类型序列化为不同的类型以及将对象反序列化为不同的对象

  本节讨论如何设计一个类型,它能将自己序列化或反序列化成一个不同的类型或对象。下面是一些例子。

●有的类型(比如Sytem.DBNull和System.Relection.Missing)设计成每个AppDomain一个实例。我们把这些类型称为单实例(singleton)类型。给定一个DBNull的应用,序列化和反序列化它不应造成在AppDomain中新建一个DBNull对象。序列化后的引用应指向AppDomain中现有的DBNull对象。

●对于某些类型来说(比如System.Type,System.Reflection.Assembly和其他反射类型,比如MemberInfo),每个类型,每个程序集或者每个成员都只有一个实例。例如,假定一个数组中的每个元素都引用了一个MemberInfo对象,其中5个元素都引用同一个MemberInfo对象。在序列化和反序列化这个数组之后,当初引用了一个MemberInfo对象的5个元素现在还是应该引用同一个MemberInfo对象。除此之外,这些元素引用的那个MemberInfo对象还必须实际对应于AppDomain中的一个特定的成员。在轮询数据库连接对象或者其他任何类型的对象时,也可以利用这个单实例功能。

●对于远程控制的对象,CLR序列化与服务器对象相关的信息。在客户端反序列化时,会造成CLR创建一个代理对象。这个代理对象有别于服务器对象的类型,但这对于客户端的代码来说是透明的。客户端直接在代理对象上调用实例方法。然后,代理代码内部会调用远程发送给服务器,由后者实际执行请求操作。

下面来看看一些代码,他们展示了如何正确的序列化和反序列化一个单实例(singleton)类型。

        [Serializable]
        public class Singleton : ISerializable
        {
            public string m_name = "Jeff";
            public DateTime m_date = DateTime.Now;

            private static readonly Singleton m_theOnlyOne = new Singleton();

            private Singleton() { }

            public static Singleton GetInstance() { return m_theOnlyOne; }

            #region ISerializable 

            void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
            {
                //这里只用改变它的类型,其他的什么都不做
                info.SetType(typeof(SingletonSeralizationHelper));
            }

            #endregion

            //nest class
            [Serializable]
            private class SingletonSeralizationHelper : IObjectReference
            {
                #region IObjectReference 
                public object GetRealObject(StreamingContext context)
                {  
                    //这个方法在对象反序列化之后调用
                    return Singleton.GetInstance();
                }
                #endregion
            }
           //注意:特殊构造器是不必要的,因为它永远不会被调用s
        }

Singleton类代表的类型在每个AppDomain中只有一个实例。以下的测试代码证明Singleton在序列化和反序列化后,保证在AppDomain中只存在一个实例。

             private void SingletonSerializationTest()
        {
            //创建一个数组,其中多个元素都引用同一个Singleton对象
            Singleton[] a1 = new Singleton[] { Singleton.GetInstance(), Singleton.GetInstance() };
            Console.WriteLine("Are both elements refer to the same object? " + (a1[0] == a1[1])); //“true”

            using (var stream = new MemoryStream())
            {
                BinaryFormatter formatter = new BinaryFormatter();

                formatter.Serialize(stream, a1);
                stream.Position = 0;
                Singleton[] a2 = (Singleton[])formatter.Deserialize(stream);
                //证明它们和预期一样
                Console.WriteLine("Are both elements refer to the same object? " + (a2[0] == a2[1])); //“true”
                Console.WriteLine("Are All elements refer to the same object? " + (a2[0] == a2[1])); //“true”
            }
        }

  现在,通过分析代码来理解所发生的事情。Singleton加载到AppDomain中时,CLR调用它的静态构造器创建一个Singleton对象,并把它的引用保存到一个静态字段m_theOlnyOne中。Singleton类没有提供任何公共构造器,这防止了其他任何代码构造该类的其他实例。

在SingletonSerializationTest中,创建一个包含2个元素的一个数组,每个元素都是一个Singleton对象,通过Console的WriteLine方法显示“true”证明这两个元素引用了同一个对象。

  现在SingletonSerializationTest的调用格式化器的Serialize方法序列化数组及其元素。序列化第一个Singleton时,格式化器检测到Singleton实现了ISerializable接口,并调用GetObjectData方法。这个方法调用SetType,向它传递一个SingletonSerializationHelper类型,告诉格式化器将Singleton格式化成SingletonSerializationHelper对象。由于AddValue没有调用,所有没有额外的信息写入流。由于格式化器自动检测出两个数组元素都引用了同一个对象,所有格式化器只序列化了一个对象。

  序列化数组之后,SingletonSerializationTest调用格式化器的Deserialize方法。对流进行反序列化时,格式化器尝试反序列化一个SingletonSerializationHelper对象,这正是格式化器之前被“欺骗”所序列化的东西(事实上这也正是Singleton为什么不需要特殊构造器的原因,因为反序列化时的类型是SingletonSerializationHelper)。构造好SingletonSerializationHelper对象后,格式化器发现这个类型实现了System.Runtime.Serialization.IObjectReference接口。这个接口的定义如下:

public interface IObjectReference
{
    object GetRealObject(StreamingContext context);
}

如果类型实现了这个接口,格式化器会调用GetRealObject方法。这个方法返回在对象反序列化之后你真正想要的引用对象。在这个例子中,SingletonSerializationHelper类型让GetRealObject返回对AppDomain中已经存在的Singleton对象的引用。所以,当格式化器的Deserialize方法返回时,a2数组包含两个元素,两者都引用AppDomain的Singleton对象。用于帮助反序列化的SingletonSerializationHelper对象立即变得“不可达了”,将来会被垃圾回收。

  对WriteLine的第二个调用显示“true”,证明a2数组的两个元素都引用了同一个对象。第三个WriteLine调用也显示“true”,证明这两个数组中的元素引用的是同一个对象。

九,序列化代理

  前面已经讨论如何修改一个类型的实现,控制该类型如何对它本身的实例进行序列化和反序列化。然而,格式化器还允许不是“类型实现的一部分”的代码重写该类型“序列化和反序列化其对象”的方式。应用程序之所以要重写一个类的行为,主要有两方面的考虑:

●允许开发人员序列化最初没有设计成要序列化的一个类型。

●允许开发人员提供一种方式将类型的一个版本映射到类型的一个不同版本中。

简单点说,为了实现这个机制,首先要定义一个“代理类型”(surrogate type),它接受对象现有类型进行序列化和反序列化的行为。然后向格式化器登记该代理类型的一个实例,告诉格式化器代理类型要作用于现有的那个类型。格式化器检测到它正要对现有类型的一个实例进行序列化和反序列化时,会调用你的代理对象定义的方法。下面通过例子来演示这一切都是如何工作的。

序列化代理类型必须实现System.Runtime.Serialization.ISerializationSurrogate接口,它的定义如下:

[ComVisible(true)]
public interface ISerializationSurrogate
{
    void GetObjectData(object obj, SerializationInfo info, StreamingContext context);
    object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector);
}

让我们来分析该接口的一个例子。假定程序含有一些DateTime对象,其中包含计算机的本地值。如果想把DateTime对象序列化到一个流中,同时希望值用国际标准时间序列化,那么该如何操作了?(注:DateTime本身是支持序列化的,这个例子展示了利用代理技术来对DateTime进行转型(这里转成了一个String))。这样一来就可以通过网络流发送给世界上其他地方的一台计算机,使DateTime保持正确。虽然不能更改DateTime类型,但可以定义自己的序列化代理类,它能控制DateTime的序列化和反序列化方式。下面是这个代理类:

        internal sealed class UniversalToLocalTimeSerializationSurrogate : ISerializationSurrogate
        {
            #region ISerializationSurrogate
            public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
            {
                //将DateTime从本地时间转为UTC
                info.AddValue("Date", ((DateTime)obj).ToUniversalTime().ToString("u"));
            }

            public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
            {
                //将DateTime从UTC转为本地时间
                return DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime();
            }
            #endregion
        }

GetObjectData方法在这里的工作方式和ISerializable接口的GetObjectData方法差不多。唯一的区别在于,ISerializationSurrogate的GetObjectData方法多了一个obj的参数,它是对要序列化的“真实”对象的一个引用。在这个方法中,将本地时间转换成了世界时间的一个字符串,并添加到了SerializationInfo集合。

SetObjectData方法用于反序列化一个DateTime对象。调用这个方法时,需要传递一个SerializationInfo的引用,SetObjectData从这个集合中获取字符串的日期,把它解析成一个世界时间在转化成本地时间返回。

  传给SetObjectData的第一个参数obj有点奇怪。在调用SetObjectData之前,格式化器分配要代理的那个类型的实例。实例的字段全是null或0,而且没有调用对象的构造器。SetObjectData内部代码为了初始化这个实例字段,可以使用传入的SerializationInfo中的值,并让SetObjectData返回null。另外,SetObjectData可以创建一个完全不同的对象,甚至不同类型的一个对象。并返回新对象的引用。在这种情况下,不管传给SetObjectData的对象有没有发生更改,格式化器都会忽略。

  在这个例子中,UniversalToLocalTimeSerializationSurrogate类扮演了DateTime类型代理的角色。DateTime是一个值类型,所有obj参数引用一个DateTime的一个已装箱实例。大多数值类型的实例都无法修改,所以SetObjectData方法会忽略obj参数,并反回一个新的DateTime对象,其中已装好期望的值。

  到此,你肯定想问,尝试序列化/反序列化一个DateTime对象时,格式化器怎么知道要用这个ISerializatonSurrogate类型呢?下面的测试代码进行了说明:

        private void SerializationSurrogateTest()
        {
            using (var stream = new MemoryStream())
            {
                //1,构造所需的格式化器
                IFormatter formatter = new SoapFormatter();

                //2,构造一个SurrogateSelector(代理选择器)对象
                SurrogateSelector ss = new SurrogateSelector();

                //3,告诉代理选择器为DateTime使用我们的代理
                ss.AddSurrogate(typeof(DateTime), formatter.Context, 
                    new UniversalToLocalTimeSerializationSurrogate());
                //注意:Addsurrogate可多次调用来登记多个代理

                //4,告诉格式化器使用代理选择器
                formatter.SurrogateSelector = ss;

                //创建一个DateTime来表示本地时间,并序列化它
                DateTime localTimeBeforeSerialize = DateTime.Now;
                formatter.Serialize(stream, localTimeBeforeSerialize);

                //stream将UTC时间作为一个字符串显示,证明它能正常工作
                stream.Position = 0;
                Console.WriteLine(new StreamReader(stream).ReadToEnd());

                //反序列化UTC时间字符串,并把它转化为本地时间
                stream.Position = 0;
                DateTime localTimeAfterDeserialize = (DateTime)formatter.Deserialize(stream);

                //证明它能正常工作
                Console.WriteLine("localTimeBeforeSerialize={0}", localTimeBeforeSerialize);
                Console.WriteLine("localTimeAfterDeserialize={0}", localTimeAfterDeserialize); 
            }
        }

结果大致是这样的:

View Code
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<xsd:dateTime id="ref-1">
<Date id="ref-2">2012-07-18 07:25:21Z</Date>
</xsd:dateTime>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

localTimeBeforeSerialize=2012/07/18 15:25:21
localTimeAfterDeserialize=2012/07/18 15:25:21

步骤1-4执行完毕后,格式化器就准备使用它已登记的代理类型。调用格式化器的Serialize方法时,会在SurrogateSelector维护的集合(一个哈希表)中查找(要序列化的)每个对象的类型。如果发现一个匹配,就调用ISerializationSurrogate对象的GetObjectData方法来获取应该写入的流信息。

  格式化器的Deserialize方法调用时,会在SurrogateSelector中查找要反序列化对象的类型。如果发现一个匹配项,就调用ISerializationSurrogate对象的SetObjectData方法来设置要反序列化的对象中的字段。

  SurrogateSelector在内部维护了一个私有的哈希表。调用AddSurrogate时,Type和StreamingContext构成了哈希表的key,对应的value就是ISerializationSurrogate对象。如果已经存在和要添加的Type/StreamingContext相同的一个键,AddSurrogate会抛出一个ArgumentException异常。通过在键中包含一个StreamingContext,可以登记一个代理对象,它知道将DateTime对象序列化/反序列化到一个文件中;再登记一个不同的代理对象,它知道将DateTime对象序列化/反序列化到一个不同的进程中。

十,代理选择器链

  多个SurrogateSelector对象可以链接到一起。例如,可以让一个SurrogateSelector对象维护一组序列化代理,这些序列化代理(surrogate)用于将类型序列化成代理(Proxy),以便通过网络传送,或者跨越AppDomain传送。还可以让另一个SurrogateSelector对象维护一组序列化代理,这些序列化代理用于将版本1的类型转化为版本2的类型。

  如果有多个希望格式化器使用的SurrogateSelector对象,必须把它们链接到一个链表中。SurrogateSelector类实现了ISurrogateSelector接口,该接口定义了三个方法。这些方法全部跟链接有关。下面是它的定义:

public interface ISurrogateSelector
{
    void ChainSelector(ISurrogateSelector selector);
    ISurrogateSelector GetNextSelector();
    ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector);
}

ChainSelector方法紧接在当前操作的ISurrogateSelector对象(this对象)之后插入一个ISurrogateSelector对象。GetNextSelector方法返回对象链表中的下一个ISurrogateSelector对象,如果当前操作是链表的链尾,就返回null。

  GetSurrogate方法在this代表的ISurrogateSelector对象中查找一对Type/StreamingContext,如果没有找到这个Type/StreamingContext对,就访问链表的下一个ISurrogateSelector对象……以此类推。如果找到一个匹配项,就返回这个ISerializationSurrogate对象,该对象负责对找到的类型的序列化和反序列化。除此之外,GetSurrogate还会返回包含匹配项的ISurrogateSelector对象,一般都用不着这个对象,所有一般将其忽略。如果链中所有的ISurrogateSelector对象都不包含匹配的Type/StreamingContext对,GetSurrogate将返回null。

  注意:FCL定义了一个ISerializationSurrogate接口,还定义了一个实现了该接口的SerializationSurrogate类型。然而,只有在一些非常罕见的情况下,才需要定义自己的类型来实现ISerializationSurrogate接口。实现ISerializationSurrogate接口的唯一理由就是将一个类型映射到一个类型时,需要更大的灵活性。System.Runtime.Remoting.Messaging.RemotingSurrogateSelector就是一个很好的例子。出于远程访问目的而序列化对象时,CLR使用RemotingSurrogateSelector来格式化对象,这个代理选择器以一种特殊的方式序列化从System.MarshalByRefObject派生的所有对象,确保反序列化会造成在客户端创建代理对象(proxy object)。

十一,反序列化时重写程序集和/或类型

  序列化一个对象时,格式化器输出类型及其程序集的全名。反序列化一个对象时,格式化器根据这个信息来准确判断要为那个对象构造并初始化什么类型。前面,我们讨论了如何利用ISerializationSurrogate接口来接管一个特定类型的序列化和反序列化工作。实现了ISerializationSurrogate接口的类型与特定程序集的一个特定类型关联在了一起。

  但这某些时候,ISerializationSurrogate机制的灵活性显得差了一些。在下面的例举中,有必要将一个对象反序列化成和序列化时不同的一个类型。

●开发人员可能把一个类型的实现从一个程序集移动到了另一个程序集。例如,程序集的版本号变化造成新程序集有别于原始程序集。

●服务器上的一个对象序列化到发送个客户端的一个流中。客户端处理流时,可以将对象反序列化成一个完全不同的类型,该类型的代码知道如何向服务器的对象发出远程方法调用。

●开发人员创建了类型的一个新版本,我们想把已经序列化的对象反序列化成类型的新版本。

利用System.Runtime.Serialization.SerializationBinder类,可以非常简单地将一个对象反序列化成一个不同的类型。为此首先要定义一个自己的类型,让它从SerializationBinder类派生。在下面的代码中,假定你的版本1.0.0.0的程序集定义了一个Ver1的类,并假定了程序集的新版本定义了Ver1ToVer2SerializationBinder类,还定义了一个Ver2类:

        internal sealed class Ver1ToVer2SerializationBinder : SerializationBinder {

            public override Type BindToType(string assemblyName, string typeName)
            {
                //将任何Ver1对象从版本1.0.0.0反序列化成一个Ver2对象

                //计算定义Ver1的程序集名称
                AssemblyName assemVer1 = Assembly.GetExecutingAssembly().GetName();
                assemVer1.Version = new Version(1, 0, 0, 0);

                if (assemblyName == assemVer1.ToString() && typeName == "Ver1")
                {
                    //如果从v1.0.0.0反序列化Ver1对象,就把它转为Ver2对象
                    return typeof(Ver2);
                }
                //否则,就只返回请求的类型
                return Type.GetType(string.Format(typeName, assemblyName));
            }
        }

现在,构造好一个格式化器之后,构造一个Ver1ToVer2SerializationBinder的一个实例,并设置格式化器的属性Binder,让它引用绑定器(binder)对象。设置好Binder属性后,调用格式化器的Deserialize方法。在反序列化期间,格式化器发现已经设置了一个绑定器。每个对象要反序列化时,格式化器都要调用绑定器的BindToType方法,向它传递程序集名称以及格式化器想要反序列化的类型。然后,BindToType判断实际应该构建什么类型,并返回这个类型。接着继续执行Ver2反序列化的那一套流程(比如Ver2如果实现了ISerializeble接口,则会调用Ver2的特殊构造器,在构造器方法中可以捕获SerializationException异常来判断是否需要转型,然后在Catch块中可以重建Ver2的实例。最后是调用Deserialized或IDeserializationCallback的OnDeserialization方法。如果没有实现这个接口,就看Ver2是否和Ver1字段是否兼容(包括类型和名称),如果不兼容则反序列化失败,抛出SerializationException异常)。

注意:SerializationBinder类还可以重写BindToName方法,从而在序列化一个对象时更改程序集/类型信息,这个方法定义如下:

    public virtual void BindToName(Type serializedType, out string assemblyName, out string typeName);

序列化期间,格式化器调用这个方法,传递它想要序列化的类型。然后,你可以通过两个out参数返回你真正想要序列化的程序集和类型。如果两个out参数返回null(默认就是这样),就不执行任何更改。

 

(全文完)