代码改变世界

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

2006-10-15 01:53 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏
  “对于复杂数据类型的支持”,可能某些朋友看了这个副标题会想,Atlas访问Web Services方法不是能够方便地支持复杂数据类型吗?为什么还需要单独花笔墨来讲解呢?的确,我在之前的文章《深入Atlas系列:Web Sevices Access in Atlas(3) - 服务器端支持(下)》里也给出了简单的范例来说明Atlas访问Web Services是能够支持复杂数据类型了的。但是,这些就够了吗?

我们首先来考虑一下下面这些问题,大家都有答案吗?

1、Atlas访问Web Services方法时能够支持所有的类型吗?
2、Atlas访问Web Services方法时能够支持非常复杂的数据类型吗?
3、Atlas访问Web Services方法时如果参数对象出现循环引用该怎么办?
4、……

一般来说,我们可能能够为普通的Atlas应用程序设计独立的数据类型和Web Services来估计回避上面这些问题,但是如果我们由于某些原因必须兼容现有的数据类型和Web Services呢?虽然事实上,我依旧建议大家为Atlas单独设计Web Services方法,因为后面提到的解决方案,从某些角度来说,开发成本有时候还是会比较高。

这次,我们首先通过几个例子来看看上面那些问题,对我们面临的一些问题有一个直观的认识:



问题1:Atlas支持范型类型作为参数吗?

很显然,用ASP.NET开发Web Services能够良好的支持范型类型作为参数,而Atlas对它的支持又如何呢?例如:我们开发了如下两个类:
Company与Employee类代码

又开发了一个Web Services方法使用:
CountEmployee方法

然后我们写一个页面来测试它,首先是HTML:
HTML代码

点击按钮会调用Javascript函数,我们来看一下简单的Javascript代码:
Javascript代码

这段Javascript构造了一个Company数组,并将其作为参数来调用Web Services方法。运行页面,猜猜看点击按钮之后会显示什么呢?能得到正确结果吗?答案是否定的。我们来看一下点击按钮后出现的错误信息:


我在之前的文章《深入Atlas系列:Web Sevices Access in Atlas(3) - 服务器端支持(下)》里曾经提到,Atlas在访问Web Services方法时,会将一个字符串变成一个和JSON表现方式非常相像的Dictionary与List结合的数据结构。由于Atlas无法将一个表示Company对象(确切说,是我们想让它来表示Company对象)的Dictionary“转换”为Company对象,因此出现了上面的错误。


问题2:Atlas不支持范型类型作为Web Services方法参数吗?

答案是否定的。我们把上面的例子作一点修改,把Web Service方法CountEmployee的参数改成范型接口::
修改后的CountEmployee方法

重新打开页面,点击按钮。嘿,出现正确结果了:


因此,Atlas是支持范型作为Web Services方法参数的,不是吗?


问题3:Atlas不能支持范型的“类”作为参数类型吗?

我们还是在例1的基础上进行修改,这次改动的是Javascript函数:
修改后的clickButton代码

我们为对象增加了“__serverType”的值“Jeffz.ComplexType.Company”,告诉服务器端这个对象表示的类型。

重新打开页面,点击按钮,也得到了正确结果:




问题4:只要增加“__serverType”就足够了吗?

在之前的例子里,我们构造Company数组时,要么使用了空数组,要么填充了null。也就是说我们从来没有构造过Employee类的对象,我们现在就构造一下,既然Company类的成员变量Employees也是个范型类,那么我们也使用“__serverType”来标明类型吧。

Javascript代码修改如下:
修改后的clickButton函数

重新打开页面,点击按钮。出现错误了:


以后就会知道,这是因为Atlas服务器端无法识别Employee类型而产生的错误。


问题5:那么该如何解决对象成员是List<Employee>类型的问题呢?

Atlas在这点上的行为比较奇怪。在例4的基础上修改Company代码,将Employees从成员变量,改为一个属性:
修改后的Company类

重新打开页面,点击按钮。正确结果又出现了!



问题6:如果参数中出现循环引用会出现什么问题?

我们先设计两个类,Boy和Girl:
Boy和Girl类代码 

再添加一个Web Services方法:
GetBoyWithGirlfriend方法

用Atlas从客户端调用它,则会发生错误:


那么如果我们在客户端构造一个对象,然后作为参数传递给Web Services方法,会怎么样呢?

在客户端构造如下代码: 
客户端Javascript代码

把它作为参数传递给任意的Web Service方法,因为它在客户端就会出现错误。点击按钮,Stack overflow:


可见,对于产生循环引用的对象,无论在客户端还是服务器端,我们都必须增加部分扩展才能支持。



例子就先举到这里,其实类似的问题还有不少,就拿以上的这些情况来说,大家都预料到了它们的结果吗?为了帮助大家弄清它们的发生原因以及解决方案,我将从Atlas相关部分的实现角度进行分析。这也就是我在这个系列里写这篇文章的原因。

首先我们整理一下从客户端发起一个Web Services方法的调用,到最后成功得到结果并将其JSON序列化(XML序列化由于是.NET Framework的“老牌”序列化方法,因此不做讨论)返回至客户端,期间所经历的使数据转变的方法调用。事实上在之前的文章内我已经提到过这一点,但是在现在,我们可能需要重新理清一下思路,如图:

  1. 在客户端构造一个对象作为参数字典。
  2. 在客户端Sys.Serialization.JSON.serialize方法将对象序列化成字符串。
  3. 字符串作为Body,用HTTP Post方法发送Request到服务器端。
  4. 服务器端获得存放在Request Body中的字符串。
  5. 使用JavaScriptObjectDeserializer.DeserializedDictionary静态方法将字符串反序列画成一个Dictionary。此Dictionary正和客户端对象的JSON表示方式完全相对应,包含了基本类型Int,String,DateTime等以及嵌套的IDictionary对象和IList对象。
  6. 使用WebServiceData.StrongTypeParameters方法根据前面的Dictionary变成了强类型的Dictionary。此Dictionary包含了真正作为参数的参数名和复杂数据类型的对象。
  7. 使用反射机制调用Web Services方法,获得方法执行后的结果对象。
  8. 使用JavaScriptSerializer.Serialize静态方法将上述对象序列化成字符串。
  9. 将字符串作为Response的Body发送至客户端。
  我们依次来分析每一个方法,以此来“挖掘”那些问题的解决方案。


1、使用客户端Sys.Serialization.JSON.serialize方法将对象序列化成字符串

Sys.Serialization.JSON对象代码如下:
Sys.Serialization.JSON对象代码

这个方法其实写得非常的清晰和简单,我的注释其实更加像是画蛇添足。另外,这个方法完全可以剥离出来利用在任何需要的地方,即使不使用Atlas客户端代码,这也是一个值得被单独取出并加以使用的方法。

该方法中最关键的地方在于第47-53行,它表明了用户可以自定义一个对象的序列化方式:只要为这个对象添加一个serialize函数即可。这也就是在客户端支持互相引用对象序列化的方式,以后将配合服务器端的支持,通过实例来详细讲解这一点。


2、使用JavaScriptObjectDeserializer.DeserializedDictionary静态方法将字符串反序列画成一个Dictionary

这个静态方法的实现其实是将参数传入JavaScriptObjectDeserializer的构造函数,并调用该实例的方法DeserializeDictionary方法。因此我们直接来分析JavaScriptObjectDeserializer(int)实例方法。
DeserializeDictionary方法

在这个方法中可以看到一个实例变量_s的使用。它是一个Microsoft.Web.Script.Serialization.JavaScriptString类型的辅助类,封装了一个字符串(就是这里需要进行反序列化操作的字符串),并提供了在反序列化过程中所需的方法(例如GetNextNonEmptyChar),从这些方法的名字内可以就可以明确地看出他们的作用,因此就不作详细分析了。它们的实现,其实也就是对字符串进行分析的过程。

在这里还出现了一个非常重要的方法就是DeserializeInternal。它虽然简单,但是负责了对于反序列化函数的选择,并最终获得反序列化后的对象。我们来看一下它的代码:
DeserializeInternal方法

这是个很标准的间接递归函数,非常容易理解:通过判断即将序列化的那个对象的类型来选择反序列化下一个对象的方法。而那些方法之中又会调用DeserializeInternal方法。最终多次递归之后,获得了反序列化后的对象。<red>请注意,到目前为止,那些对象都只是包含IDictionary,IList以及一切基础类型的对象,还不能直接给Web Service方法使用。</red>至于那些特定的序列化方法,代码非常简单也容易理解,就不做太多的分析了。:)

另外,在DeserializeInternal方法中可以看到它将传入的depth参数与_depthLimit成员变量进行比较,如果超过了_depthLimit的值,则会抛出异常。该值可以在Web.config文件中通过类似下面的方式进行配置:
<webServices enableBrowserAccess="true" objectDeserializerDepthLimit="10" />

这样,就将_depthLimit成员变量设为了10。这个值的作用是为了控制服务器端反序列化一个对象的深度。它的默认值是100,也就是说,其默认值几乎也已经满足所有应用的需要,一般来说我们不会去对它进行设置。Atlas对于Web Services相关的配置不只有这个,不过在这里就不作讨论了。在这个系列今后的文章中,我会结合实例一一分析Web Services相关配置的功能以及使用方式。

正如前面所提到的,最后得到的Dictionary和客户端传入的参数(例如使用Sys.Net.ServiceMethod.invoke静态方法时的第4个参数)的JSON表示方式完全对应,包含了基本类型Int,String和DateTime(分别对应了Javascript类型中的Number,String和Date类型)的对象,以及嵌套的IDictionary和IList对象(分别对应了JSON表示中的“{}”和“[]”)。

有了这些数据,就能通过WebServiceData.StrongTypeParameters方法来获得真正用于函数调用的参数了。显然,这个方法会根据Web Services方法的参数类型进行转换。在之前获得的完全相同的Dictionary对象,为了配合不同的Web Services方法调用,会被转化为不同的类型对象,最后的结果甚至会大相径庭。可以想象的到,WebServiceData.StrongTypeParameters方法非常复杂,非常有技巧,也是最有扩展性的一个方法。它可以说是Atlas支持客户端Web Services方法调用中使用复杂类型的核心之一。

那么它究竟是如何工作的?我们将在下一篇文章中讨论这个问题。