Hello World
Spiga

深入Atlas系列:Web Sevices Access in Atlas(5) - 对于复杂数据类型的支持(中)

2006-10-15 21:43 by 老赵, 2379 visits
  在前一片文章里,我们已经得到了从客户端获得的字符串反序列化以后的Dictionary,我们知道它的形式和JSON非常的相像,只是存放了一些基本类型的数据还有IDictionary已经IList对象。它们在被应用到真正用于调用Web Services方法之前,还需要将这个Dictionary里的数转化为强类型的参数对象。在Atlas中,是使用了WebServiceMethodData.StrongTypeParameters方法来获得一个包含强类型参数对象的字典的。因此我们直接开始分析这个方法吧。

一个包含着JSON信息的Dictionary已经作为rawParams参数被传入该方法,代码如下:
StrongTypeParameters方法

我们可以看到,真正起作用的方法是ObjectConverter.ConvertToType静态方法,它通过传入的对象信息,目标类型来获得最终我们需要的参数对象。那么this.Owner,这个WebServiceData对象的作用是什么呢?我们先来简单看一下ObjectConverter.ConvertToType静态方法的代码:
ConvertToType静态方法

可以发现,WebServiceData对象被作为了IJavaScriptSerializationContext类型的对象来使用。Microsoft.Web.Script.Serialization.IJavaScriptSerializationContext接口中最重要的方法就是GetType(string),它通过给定一个字符串形式的类型名,来获得这个类型对象Type。很自然,这个类型首先需要被IJavaScriptSerializationContext所识别。还记得在我的前一篇文章《深入Atlas系列:Web Sevices Access in Atlas(4) - 对于复杂数据类型的支持(上)》中“问题4”的错误吗?它的产生原因就是因为那个WebServiceData对象无法识别字符串“Jeffz.ComplexType.Employee”所表示的类型。既然无法识别,那么即使使用了“__serverType”来表示一个客户端对象的服务器端类型,也是无法转化的。那么为什么会无法识别这个对象呢?我们继续追踪,分析ConvertObjectToType方法的实现,代码如下:
ConvertObjectToType方法

从这里开始,Atlas对于支持复杂类型的扩展性一点点地被揭开了。在进行转换时,首先会调用JavaScriptConverter.ConverterExistsForType来检验是否有支持该Type的JavaScriptConverter。这里的JavaScriptConverter是指一个继承了Microsoft.Web.Script.Serialization.JavaScriptConverter抽象类的对象。在Atlas中可以可以根据配置文件来加载需要的JavaScriptConverter。事实上,在默认的Web.config文件中,已经为DataSet、DataTable和DataRow三个类写好了JavaScriptConverter。我们在Web.config类中可以发现这样的配置元素:
<microsoft.web>
    
<converters>
        
<add type="Microsoft.Web.Script.Serialization.Converters.DataSetConverter"/>
        
<add type="Microsoft.Web.Script.Serialization.Converters.DataRowConverter"/>
        
<add type="Microsoft.Web.Script.Serialization.Converters.DataTableConverter"/>
    
</converters>
    ……
</microsoft.web>

虽然没有在配置文件内将对象类型与JavaScriptConverter对应起来,但是在Microsoft.Web.Script.Serialization.JavaScriptConverter抽象类中有个virtual属性SupportedTypes,它返回了这个JavaScriptConverter能够支持的Type数组,默认为null。JavaScriptConverter就是依靠这个把具体的JavaScriptConverter子类与某个特定的类型关联起来。

在调用ObjectConverter.ConvertObjectToType方法时,首先会查找是不是已经有了目标type所对应的JavaScriptConverter(第11行),如果有的话,则会通过JavaScriptObjectSerializer.Serialize静态方法将需要转换的对象o序列化成字符串,然后再调用JavaScriptConverter.DeserializeString静态方法,将该字符串反序列化成Web Services方法所需要的参数对象。值得注意的是,在这两个静态方法中,都会再次查看是否有对应的JavaScriptConverter与所操作的对象相对应,如果有,则使用JavaScriptConverter进行操作,反之则使用Atlas内定的操作方法。

可是,是不是有朋友对于ObjectConverter.ConvertObjectToType方法中第13-16行的逻辑表示不解呢?为什么需要将传入的参数o通过它对应的JavaScriptConverter重新序列化成字符串,再交给目标类型的JavaScriptConverter再反序列化一次呢?其实这个思路似乎有点被JavaScriptObjectSerializer.Serialize静态方法的实现所误导了(确切的说是被在这里对于该静态方法的使用所误导)。这个静态方法的确会检查是否有与o的类型对应的JavaScriptConverter存在,但是我们仔细思考一下,这个o的类型会是什么呢?

这里的参数o是刚从JSON字符串转变过来的表示参数的对象之一,它的作用是“表现参数”。它的类型其实只是Int32,Double,String,DateTime等基本数据类型或者IDictionary以及IList对象中的一种。我想,开发人员应该不太会为这写类型写JavaScriptConverter吧?对于这两种对象的转换,Atlas已经支持地相当出色了。事实上,在Atlas中的确也没有为基础类型或者IDictionary以及IList这写类型准备JavaScriptConverter。在ObjectConverter.ConvertObjectToType方法中第14行,使用JavaScriptObjectSerializer.Serialize静态方法序列化o对象,其实只是将这个对象再转变回字符串,转变为客户端通过Request Body发送过来的原始字符串。

这样的话,逻辑就顺利了。将一个类型与JavaScriptConverter所绑定的目的,是为了能够自定义关于这个对象的序列化已经反序列化的操作。这两个操作应该是一体的。这个类需要负责将某个类型的对象输出成特别的JSON代码,也需要输出一段Javascript来告知客户端该如何序列化一个客户端对象(通过提供对象一个serialize函数,这一点在上一篇文章中提到过),在以后分析Atlas为某个Web Services生成客户端代理的原理时会详细说明这一点。可以想象,如果(仅仅是如果)在这之间,对象o找到了对应的JavaScriptConverter,得到的序列化字符串并不是客户端的原始输入,那么为目标Type设计的JavaScriptConverter在反序列化一个字符串时就会失败,或者导致不可预料的结果。

因此,其实JavaScriptConverter.DeserializeString方法接收到的字符串,应该(或者说在正常的情况下)就是客户端表示该对象的原始输入字符串。这样,我们就能做到对一个类型自定义一组扩展。这个方法常常被用于序列化于反序列化一组相互引用的对象,在下一片文章中,我将以范例来具体说明这一点。事实上,Atlas里已有的三个JavaScriptConverter,就是为了处理三个复杂类型DataSet,DataTable以及DataRow的序列化与反序列化操作,它们就是存在着相互引用的对象,Atlas也通过了添加JavaScriptConverter来处理这个问题。

如果没有找到目标type所对应的JavaScriptConverter,那么ObjectConverter.ConvertObjectToType方法就会使用Atlas的默认操作——或者说内部操作:ObjectConverter.ConvertObjectToTypeInternal方法来将o转换为目标type的对象,以便Web Services方法使用。接下来,我们来看一下它的实现:
ConvertObjectToTypeInternal方法

如果当前需要转换的对象为IDictionary类型或者IList类型,那么则调用相应的方法进行转换,这点稍后再作分析。如果这些o不是这两种类型,则说明它们是些基本类型了,例如Int32,Double,DateTime,Boolean等。在这里我们看到Atlas允许我们进行另外一种可以使用的扩展方式:使用自定义的System.ComponentModel.TypeConverter子类并使用System.ComponentModel.TypeConverterAttribute为类型的转换进行标注。熟悉ASP.NET控件开发的朋友可能会比较熟悉这个做法,为控件属性提供设计期支持时经常会使用这种做法。在Atlas中,如果我们的类型会从那些基本类型转换过来,那么我们就可以提供自定义TypeConverter的方式进行扩展。在今后的文章中,我将提供示例供大家参考。

在通过TypeDescriptor.GetConverter方法获得目标类型的TypeConverter后,会查看该TypeConverter是否能够从目前o对象的类型进行转换。如果可以,则返回转换后的结果。否则的话,则设法先将o对象转化为字符串,再使用TypeConverter的ConvertFromInvariantString方法进行转换。请注意,这里使用的方法都是XXXInvariantString,也就是说,它们是与系统的Culture相关的。举个例子,如果我们需要自定一个DateTime到一个特定类型的转换,传入TypeConverter中ConvertFromInvariantString方法的字符串参数,它的值会根据系统当前Culture环境的不同而产生变化。

事实上,对于目标类型本身就是参数类型(例如Int32)的情况,这段代码一般就会执行第36-39行的逻辑。例如对象o的类型是long,而目标类型为int,则这段代码就会先使用Int64Converter将o转变为字符串,再通过Int32Converter将这个字符串转化为我们需要的参数类型:Int32。

另外,通过理解这个逻辑,我们也能够解释下面的行为的原因:

如果参数为整型,那么Atlas会自动将客户端传入的字符串参数转化为整型以保证Web Services方法调用成功。例如,在我之前的文章《深入Atlas系列:Web Sevices Access in Atlas - 示例一 - 特别的访问方式》的范例中,我将txtAge的text属性绑定到serviceMethod的参数age上。这就相当于将一个字符串的值赋于Web Services方法的整型参数,通过使用Fiddler查看HTTP传输的数据也能发现,在Request Body中传递的数据也类似与“{ age : "20", ... }”。显然,在服务器端得到的age参数输入,其类型是一个字符串。但是,就是因为之前描述的逻辑,在真正使用这个字符串的值去进行Web Services方法调用之前,会被Int32Converter转换成为整型。这样,方法调用就成功了。

如果无法使用TypeConverter将对象o转换为目标类型的话,就尝试着直接将o使用在方法调用上了。在上面的代码中可以发现,Atlas使用了目标type的IsAssignableFrom方法来检测“直接赋值”的合法性。最后,如果发现对象o,不管使用什么方式都无法将其转换为目标type的话,则抛出了InvalidOperationException,表示类型转换的失败。

刚才详细讨论的逻辑是在最后对于一个“值”的转换。但是一般来说,在Atlas转换数据类型时难免会遇到IDictionary和IList对象的嵌套。在这时候,就要使用代码所看到的ConvertDictionaryToObject方法和ConvertListToObject方法了。首先我们来分析一下ConvertListToObject方法代码:
ConvertListToObject方法

这个方法的作用是将IList对象list转换成type类型的对象。自然,它在实际使用中也是一个列表或者数组。在这个方法中,根据Type格式的不同,转换后的数据类型可能是ArrayList,Array或者Generic类型List<T>。可以简单地认为,在Atlas中所支持的“列表”类型或者“数组”类型就只有这些。因此,我们在Web Services方法中应该使用这些以及它们的兼容类型,例如IList<T>,ICollection<T>或者IList等。对于无法转换的数据类型,只能不得已地抛出InvalidOperationException了。我们在使用Atlas访问Web Services方法时应该完全避免使用这样的数据类型。当然,我们能够添加自己的扩展,以支持我们需要的数据类型。在下一片文章中我将给出相应的范例。

在上面的方法中,我们可以看到,它总是调用了AddItemToList方法来填充一个“列表”对象。其实我们也能够想象得到它的实现方式:它枚举了源列表的每个元素,调用ConvertObjectToType方法将其转换成合适的对象,然后将其填充至目标列表中。对象的转换总是在大量的间接递归中实现的。

与上述的ConvertListToObject方法相对应是ConvertDictionaryToObject方法。相比于前者返回的是一个“列表”,而后者则返回的则好比是一个“元素”的对象。代码如下:
ConvertDictionaryToObject方法

在这里可以发现,如果客户端传来的对象指定了__serverType,那么将使用__serverType所表示的值从IJavaScriptSerializationContext对象中获得真正使用的类。请注意,它和目标type所表示的类,完全可以不是同一个!产生的对象是目标type的接口实现亦可,产生的对象是目标type的子类亦可,只要能够正确地调用Web Services方法的类型都可以。这就给了我们对于使用Atlas调用Web Services方法有了更高的灵活性,其能力甚至已经凌驾于Web Services本身!这就是Atlas的强大之处。以后将会出现与此相关的示例,敬请留意。:)

等拿到了真正的目标对象类型后,就使用Activator.CreateInstance方法进行对象构造。最后枚举Dictionary中的每一个key,并将以之对应的value和对象传入AssignToPropertyOrField方法进行public属性或者成员变量的赋值。该方法首先再次使用ConvertObjectToType方法递归地将value转换为合适的对象,然后尝试寻找与key对应的属性的set方法,如果没有的话,则再寻找与key对应的Public Field,最后设值。

就这样,一个对象就成功地被构造出来了。



通过分析这篇文章的代码,我们至少可以发现一下几点:
  1. 在Atlas中,通过实现JavaScriptConverter的子类可以自定一个特定类型的序列化和反序列化能力。
  2. 在Atlas中,通过实现TypeConverter并使用TypeConverterAttribute标记可以为一个特定类型,自定义从基本类型(例如Int32,Double,DateTime,Boolean等)的反序列化的能力。
  3. Atlas访问Web Services方法对于范型类型(甚至对于普通数据类型)的支持,是不完整的,不过已经足够大多数应用的需求。而且对于不支持的数据类型,我们可以通过自定义的方式提供对它们的支持。
  4. Atlas访问Web Services方法有非常灵活的可控性,它的功能甚至超过普通的Web Services访问,能够做到普通Web Services无法做到的有用功能。
  在今后的文章中,我会一一提供对于这些功能的示例。



到现在为止,我们对于服务器端反序列化客户端数据的方式已经分析地差不多了,不过还缺少对于那个IJavaScriptSerializationContext,也就是那个WebServiceData对象中GetType方法的分析与理解。那么这个对象这个方法到底是如何工作的呢?另外,得到了调用Web Services方法的结果以后,它又是怎么被序列化并输出是客户端的呢?

我们将在下一篇文章中讨论这些问题。
Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

7 条回复

  1. 老赵
    admin
    链接

    老赵 2006-10-15 21:50:00

    我决定从现在开始将“深入Atlas系列”分为“分析”与“示例”两部分。
    “分析”部分文章可能会比较“枯燥”,因为它几乎完全就是分析,或者只有一些无关痛痒的示例,但是这才是我精力花费最集中的地方。这些文章都是“示例”部分的实现依据。而“示例”部分文章则使用了“分析”所得到的结论,然后设计出来的Atlas使用范例。当然,为了保持这个系列的“深入”性,这些范例大都会是文档中没有的使用方式,而不仅仅是流于表面的示例。
    我想这样将可以方便对于两种内容有不同兴趣的朋友有选择性地进行阅读。

  2. 小蜗牛
    *.*.*.*
    链接

    小蜗牛 2006-10-15 22:40:00

    辛苦了

  3. 老赵
    admin
    链接

    老赵 2006-10-15 22:44:00

    @小蜗牛
    :)
    不过的确有点累……

  4. Dflying Chen
    *.*.*.*
    链接

    Dflying Chen 2006-10-15 23:32:00

    @Jeffrey Zhao
    换马甲了?

  5. 老赵
    admin
    链接

    老赵 2006-10-15 23:47:00

    @Dflying Chen
    嗯?换了个主题。
    上次那个好像在FireFox下面看上去很丑,现在清爽多了。

  6. Dflying Chen
    *.*.*.*
    链接

    Dflying Chen 2006-10-16 00:11:00

    @Jeffrey Zhao
    我是说“小蜗牛 ”…………

  7. 老赵
    admin
    链接

    老赵 2006-10-16 00:13:00

    @Dflying Chen
    你说这个阿……其实两个级别差不多的,都不是mj……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我