深入Atlas系列:Web Sevices Access in Atlas(4) - 对于复杂数据类型的支持(上)
2006-10-15 01:53 by 老赵, 3105 visits
“对于复杂数据类型的支持”,可能某些朋友看了这个副标题会想,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对它的支持又如何呢?例如:我们开发了如下两个类:
又开发了一个Web Services方法使用:
然后我们写一个页面来测试它,首先是HTML:
点击按钮会调用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的参数改成范型接口::
重新打开页面,点击按钮。嘿,出现正确结果了:
因此,Atlas是支持范型作为Web Services方法参数的,不是吗?
问题3:Atlas不能支持范型的“类”作为参数类型吗?
我们还是在例1的基础上进行修改,这次改动的是Javascript函数:
我们为对象增加了“__serverType”的值“Jeffz.ComplexType.Company”,告诉服务器端这个对象表示的类型。
重新打开页面,点击按钮,也得到了正确结果:
问题4:只要增加“__serverType”就足够了吗?
在之前的例子里,我们构造Company数组时,要么使用了空数组,要么填充了null。也就是说我们从来没有构造过Employee类的对象,我们现在就构造一下,既然Company类的成员变量Employees也是个范型类,那么我们也使用“__serverType”来标明类型吧。
Javascript代码修改如下:
重新打开页面,点击按钮。出现错误了:
以后就会知道,这是因为Atlas服务器端无法识别Employee类型而产生的错误。
问题5:那么该如何解决对象成员是List<Employee>类型的问题呢?
Atlas在这点上的行为比较奇怪。在例4的基础上修改Company代码,将Employees从成员变量,改为一个属性:
重新打开页面,点击按钮。正确结果又出现了!
问题6:如果参数中出现循环引用会出现什么问题?
我们先设计两个类,Boy和Girl:
再添加一个Web Services方法:
用Atlas从客户端调用它,则会发生错误:
那么如果我们在客户端构造一个对象,然后作为参数传递给Web Services方法,会怎么样呢?
在客户端构造如下代码:
把它作为参数传递给任意的Web Service方法,因为它在客户端就会出现错误。点击按钮,Stack overflow:
可见,对于产生循环引用的对象,无论在客户端还是服务器端,我们都必须增加部分扩展才能支持。
例子就先举到这里,其实类似的问题还有不少,就拿以上的这些情况来说,大家都预料到了它们的结果吗?为了帮助大家弄清它们的发生原因以及解决方案,我将从Atlas相关部分的实现角度进行分析。这也就是我在这个系列里写这篇文章的原因。
首先我们整理一下从客户端发起一个Web Services方法的调用,到最后成功得到结果并将其JSON序列化(XML序列化由于是.NET Framework的“老牌”序列化方法,因此不做讨论)返回至客户端,期间所经历的使数据转变的方法调用。事实上在之前的文章内我已经提到过这一点,但是在现在,我们可能需要重新理清一下思路,如图:
1、使用客户端Sys.Serialization.JSON.serialize方法将对象序列化成字符串
Sys.Serialization.JSON对象代码如下:
这个方法其实写得非常的清晰和简单,我的注释其实更加像是画蛇添足。另外,这个方法完全可以剥离出来利用在任何需要的地方,即使不使用Atlas客户端代码,这也是一个值得被单独取出并加以使用的方法。
该方法中最关键的地方在于第47-53行,它表明了用户可以自定义一个对象的序列化方式:只要为这个对象添加一个serialize函数即可。这也就是在客户端支持互相引用对象序列化的方式,以后将配合服务器端的支持,通过实例来详细讲解这一点。
2、使用JavaScriptObjectDeserializer.DeserializedDictionary静态方法将字符串反序列画成一个Dictionary
这个静态方法的实现其实是将参数传入JavaScriptObjectDeserializer的构造函数,并调用该实例的方法DeserializeDictionary方法。因此我们直接来分析JavaScriptObjectDeserializer(int)实例方法。
在这个方法中可以看到一个实例变量_s的使用。它是一个Microsoft.Web.Script.Serialization.JavaScriptString类型的辅助类,封装了一个字符串(就是这里需要进行反序列化操作的字符串),并提供了在反序列化过程中所需的方法(例如GetNextNonEmptyChar),从这些方法的名字内可以就可以明确地看出他们的作用,因此就不作详细分析了。它们的实现,其实也就是对字符串进行分析的过程。
在这里还出现了一个非常重要的方法就是DeserializeInternal。它虽然简单,但是负责了对于反序列化函数的选择,并最终获得反序列化后的对象。我们来看一下它的代码:
这是个很标准的间接递归函数,非常容易理解:通过判断即将序列化的那个对象的类型来选择反序列化下一个对象的方法。而那些方法之中又会调用DeserializeInternal方法。最终多次递归之后,获得了反序列化后的对象。<red>请注意,到目前为止,那些对象都只是包含IDictionary,IList以及一切基础类型的对象,还不能直接给Web Service方法使用。</red>至于那些特定的序列化方法,代码非常简单也容易理解,就不做太多的分析了。:)
另外,在DeserializeInternal方法中可以看到它将传入的depth参数与_depthLimit成员变量进行比较,如果超过了_depthLimit的值,则会抛出异常。该值可以在Web.config文件中通过类似下面的方式进行配置:
这样,就将_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方法调用中使用复杂类型的核心之一。
那么它究竟是如何工作的?我们将在下一篇文章中讨论这个问题。
我们首先来考虑一下下面这些问题,大家都有答案吗?
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类代码
1 namespace Jeffz.ComplexType
2 {
3 public class Company
4 {
5 public string Name;
6
7 public List<Employee> Employees;
8 }
9
10 public class Employee
11 {
12 public string Name;
13
14 public int Age;
15 }
16 }
17
1 namespace Jeffz.ComplexType
2 {
3 public class Company
4 {
5 public string Name;
6
7 public List<Employee> Employees;
8 }
9
10 public class Employee
11 {
12 public string Name;
13
14 public int Age;
15 }
16 }
17
又开发了一个Web Services方法使用:
CountEmployee方法
1 [WebService(Namespace = "http://tempuri.org/")]
2 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
3 public class ComplexTypeWS : System.Web.Services.WebService {
4
5 [WebMethod]
6 public string CountEmployee(List<Company> companies)
7 {
8 StringBuilder sb = new StringBuilder();
9 foreach (Company company in companies)
10 {
11 sb.AppendFormat("Company '{0}' has {1} employee(s).<br />", company.Name, company.Employees.Count);
12 }
13
14 return sb.ToString();
15 }
16 }
1 [WebService(Namespace = "http://tempuri.org/")]
2 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
3 public class ComplexTypeWS : System.Web.Services.WebService {
4
5 [WebMethod]
6 public string CountEmployee(List<Company> companies)
7 {
8 StringBuilder sb = new StringBuilder();
9 foreach (Company company in companies)
10 {
11 sb.AppendFormat("Company '{0}' has {1} employee(s).<br />", company.Name, company.Employees.Count);
12 }
13
14 return sb.ToString();
15 }
16 }
然后我们写一个页面来测试它,首先是HTML:
HTML代码
1 <atlas:ScriptManager runat="server" ID="ScriptManager1" />
2
3 <input type="button" id="button" onclick="clickButton()" value="Click me!" />
4 <div style="font-weight:bold;">Information:</div>
5 <div id="display"></div>
1 <atlas:ScriptManager runat="server" ID="ScriptManager1" />
2
3 <input type="button" id="button" onclick="clickButton()" value="Click me!" />
4 <div style="font-weight:bold;">Information:</div>
5 <div id="display"></div>
点击按钮会调用Javascript函数,我们来看一下简单的Javascript代码:
Javascript代码
1 <script language="javascript">
2 function clickButton()
3 {
4 var companyArray = new Array();
5 companyArray.push({Name : "Company1", Employees : [] });
6 companyArray.push({Name : "Company2", Employees : [null] });
7
8 Sys.Net.ServiceMethod.invoke(
9 "ComplexTypeWS.asmx",
10 "CountEmployee",
11 null,
12 { companies : companyArray },
13 onComplete
14 )
15
16 $("display").innerHTML = "";
17 }
18
19 function onComplete(result)
20 {
21 $("display").innerHTML = result;
22 }
23 </script>
24
1 <script language="javascript">
2 function clickButton()
3 {
4 var companyArray = new Array();
5 companyArray.push({Name : "Company1", Employees : [] });
6 companyArray.push({Name : "Company2", Employees : [null] });
7
8 Sys.Net.ServiceMethod.invoke(
9 "ComplexTypeWS.asmx",
10 "CountEmployee",
11 null,
12 { companies : companyArray },
13 onComplete
14 )
15
16 $("display").innerHTML = "";
17 }
18
19 function onComplete(result)
20 {
21 $("display").innerHTML = result;
22 }
23 </script>
24
这段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方法
1 [WebMethod]
2 public string CountEmployee(IList<Company> companies)
3 {
4 StringBuilder sb = new StringBuilder();
5 foreach (Company company in companies)
6 {
7 sb.AppendFormat("Company '{0}' has {1} employee(s).<br />", company.Name, company.Employees.Length);
8 }
9
10 return sb.ToString();
11 }
1 [WebMethod]
2 public string CountEmployee(IList<Company> companies)
3 {
4 StringBuilder sb = new StringBuilder();
5 foreach (Company company in companies)
6 {
7 sb.AppendFormat("Company '{0}' has {1} employee(s).<br />", company.Name, company.Employees.Length);
8 }
9
10 return sb.ToString();
11 }
重新打开页面,点击按钮。嘿,出现正确结果了:
因此,Atlas是支持范型作为Web Services方法参数的,不是吗?
问题3:Atlas不能支持范型的“类”作为参数类型吗?
我们还是在例1的基础上进行修改,这次改动的是Javascript函数:
修改后的clickButton代码
1 function clickButton()
2 {
3 var companyArray = new Array();
4 companyArray.push({Name : "Company1", Employees : [], __serverType : "Jeffz.ComplexType.Company" });
5 companyArray.push({Name : "Company2", Employees : [null], __serverType : "Jeffz.ComplexType.Company" });
6
7 ……
8 }
1 function clickButton()
2 {
3 var companyArray = new Array();
4 companyArray.push({Name : "Company1", Employees : [], __serverType : "Jeffz.ComplexType.Company" });
5 companyArray.push({Name : "Company2", Employees : [null], __serverType : "Jeffz.ComplexType.Company" });
6
7 ……
8 }
我们为对象增加了“__serverType”的值“Jeffz.ComplexType.Company”,告诉服务器端这个对象表示的类型。
重新打开页面,点击按钮,也得到了正确结果:
问题4:只要增加“__serverType”就足够了吗?
在之前的例子里,我们构造Company数组时,要么使用了空数组,要么填充了null。也就是说我们从来没有构造过Employee类的对象,我们现在就构造一下,既然Company类的成员变量Employees也是个范型类,那么我们也使用“__serverType”来标明类型吧。
Javascript代码修改如下:
修改后的clickButton函数
1 function clickButton()
2 {
3 var employee = { Name : "Employee", Age : 30, __serverType : "Jeffz.ComplexType.Employee" };
4
5 var companyArray = new Array();
6 companyArray.push({Name : "Company1", Employees : [], __serverType : "Jeffz.ComplexType.Company" });
7 companyArray.push({Name : "Company2", Employees : [employee], __serverType : "Jeffz.ComplexType.Company" });
8
9 ……
10 }
1 function clickButton()
2 {
3 var employee = { Name : "Employee", Age : 30, __serverType : "Jeffz.ComplexType.Employee" };
4
5 var companyArray = new Array();
6 companyArray.push({Name : "Company1", Employees : [], __serverType : "Jeffz.ComplexType.Company" });
7 companyArray.push({Name : "Company2", Employees : [employee], __serverType : "Jeffz.ComplexType.Company" });
8
9 ……
10 }
重新打开页面,点击按钮。出现错误了:
以后就会知道,这是因为Atlas服务器端无法识别Employee类型而产生的错误。
问题5:那么该如何解决对象成员是List<Employee>类型的问题呢?
Atlas在这点上的行为比较奇怪。在例4的基础上修改Company代码,将Employees从成员变量,改为一个属性:
修改后的Company类
1 public class Company
2 {
3 public string Name;
4
5 private List<Employee> _Employees;
6
7 public List<Employee> Employees
8 {
9 get
10 {
11 return this._Employees;
12 }
13 set
14 {
15 this._Employees = value;
16 }
17 }
18 }
1 public class Company
2 {
3 public string Name;
4
5 private List<Employee> _Employees;
6
7 public List<Employee> Employees
8 {
9 get
10 {
11 return this._Employees;
12 }
13 set
14 {
15 this._Employees = value;
16 }
17 }
18 }
重新打开页面,点击按钮。正确结果又出现了!
问题6:如果参数中出现循环引用会出现什么问题?
我们先设计两个类,Boy和Girl:
Boy和Girl类代码
1 public class Boy
2 {
3 public string Name;
4
5 public Girl Grilfriend;
6 }
7
8 public class Girl
9 {
10 public string Name;
11
12 public Boy Boyfriend;
13 }
1 public class Boy
2 {
3 public string Name;
4
5 public Girl Grilfriend;
6 }
7
8 public class Girl
9 {
10 public string Name;
11
12 public Boy Boyfriend;
13 }
再添加一个Web Services方法:
GetBoyWithGirlfriend方法
1 [WebMethod]
2 public Boy GetBoyWithGirlfriend()
3 {
4 Boy boy = new Boy();
5 boy.Name = "Boy";
6 Girl girl = new Girl();
7 girl.Name = "Girl";
8
9 boy.Grilfriend = girl;
10 girl.Boyfriend = boy;
11
12 return boy;
13 }
14
1 [WebMethod]
2 public Boy GetBoyWithGirlfriend()
3 {
4 Boy boy = new Boy();
5 boy.Name = "Boy";
6 Girl girl = new Girl();
7 girl.Name = "Girl";
8
9 boy.Grilfriend = girl;
10 girl.Boyfriend = boy;
11
12 return boy;
13 }
14
用Atlas从客户端调用它,则会发生错误:
那么如果我们在客户端构造一个对象,然后作为参数传递给Web Services方法,会怎么样呢?
在客户端构造如下代码:
客户端Javascript代码
1var boy = new Object();
2boy.Name = "Boy";
3var girl = new Object();
4girl.Name = "Girl";
5
6boy.Girlfriend = girl;
7girl.Boyfriend = boy;
1var boy = new Object();
2boy.Name = "Boy";
3var girl = new Object();
4girl.Name = "Girl";
5
6boy.Girlfriend = girl;
7girl.Boyfriend = boy;
把它作为参数传递给任意的Web Service方法,因为它在客户端就会出现错误。点击按钮,Stack overflow:
可见,对于产生循环引用的对象,无论在客户端还是服务器端,我们都必须增加部分扩展才能支持。
例子就先举到这里,其实类似的问题还有不少,就拿以上的这些情况来说,大家都预料到了它们的结果吗?为了帮助大家弄清它们的发生原因以及解决方案,我将从Atlas相关部分的实现角度进行分析。这也就是我在这个系列里写这篇文章的原因。
首先我们整理一下从客户端发起一个Web Services方法的调用,到最后成功得到结果并将其JSON序列化(XML序列化由于是.NET Framework的“老牌”序列化方法,因此不做讨论)返回至客户端,期间所经历的使数据转变的方法调用。事实上在之前的文章内我已经提到过这一点,但是在现在,我们可能需要重新理清一下思路,如图:
- 在客户端构造一个对象作为参数字典。
- 在客户端Sys.Serialization.JSON.serialize方法将对象序列化成字符串。
- 字符串作为Body,用HTTP Post方法发送Request到服务器端。
- 服务器端获得存放在Request Body中的字符串。
- 使用JavaScriptObjectDeserializer.DeserializedDictionary静态方法将字符串反序列画成一个Dictionary。此Dictionary正和客户端对象的JSON表示方式完全相对应,包含了基本类型Int,String,DateTime等以及嵌套的IDictionary对象和IList对象。
- 使用WebServiceData.StrongTypeParameters方法根据前面的Dictionary变成了强类型的Dictionary。此Dictionary包含了真正作为参数的参数名和复杂数据类型的对象。
- 使用反射机制调用Web Services方法,获得方法执行后的结果对象。
- 使用JavaScriptSerializer.Serialize静态方法将上述对象序列化成字符串。
- 将字符串作为Response的Body发送至客户端。
1、使用客户端Sys.Serialization.JSON.serialize方法将对象序列化成字符串
Sys.Serialization.JSON对象代码如下:
Sys.Serialization.JSON对象代码
1 Sys.Serialization.JSON = new function() {
2
3 this.serialize = function(object) {
4 // 构造一个Sys.StringBuilder对象
5 var stringBuilder = new Sys.StringBuilder();
6 // 调用serializerWithBuilder方法序列化对象
7 serializeWithBuilder(object, stringBuilder);
8 // 返回结果
9 return stringBuilder.toString();
10 }
11
12 this.deserialize = function(data) {
13 // 简单地调用eval方法来反序列化对象
14 return eval('(' + data + ')');
15 }
16
17 function serializeWithBuilder(object, stringBuilder) {
18 var i;
19
20 // 检查传入的object类型
21 switch (typeof object) {
22 // 如果返回"object",表示该对象为Array或真正的Object
23 case 'object':
24 // 如果object不为null
25 if (object) {
26 // 如果该object为Array
27 if (Array.isInstanceOfType(object)) {
28 // 填写左方括号"["
29 stringBuilder.append('[');
30 // 枚举每一个对象
31 for (i = 0; i < object.length; ++i) {
32 // 如果不是第一个对象
33 if (i > 0) {
34 // 则添加一个逗号","
35 stringBuilder.append(',');
36 }
37 // 其实应该算作是一个Bug,
38 // 不应该是stringBuilder.append(...),
39 // 直接递归调用serializeWithBuilder方法即可,
40 // 不过stringBuilder.append(null)不影响结果
41 stringBuilder.append(serializeWithBuilder(object[i], stringBuilder));
42 }
43 // 添加右括号
44 stringBuilder.append(']');
45 }
46 else { // 如果是类型真正的Object
47 // 如果该对象有serialize函数
48 if (typeof object.serialize == 'function') {
49 // 则调用该方法获得序列化后的字符串
50 // 这是“自定义序列化”能力的关键!
51 stringBuilder.append(object.serialize());
52 break;
53 }
54 // 如果没有serialize函数
55 // 则使用默认序列化方式,添加左大括号
56 stringBuilder.append('{');
57 // 是否需要逗号的标记,初始化为false,表示第一个不需要
58 var needComma = false;
59 // 枚举每一个对象name
60 for (var name in object) {
61 // 如果由$开始(比如$index),则不序列化
62 if (name.startsWith('$')) {
63 continue;
64 }
65
66 // 获得值
67 var value = object[name];
68 // 如果该值不是undefined也不是一个函数
69 if (typeof value != 'undefined' && typeof value != 'function') {
70 // 如果需要逗号
71 if (needComma) {
72 // 则添加逗号
73 stringBuilder.append(',');
74 }
75 else {
76 // 第一个不需要逗号,则以后需要
77 needComma = true;
78 }
79
80 // 首先添加name,和之前一样,无须stringBuilder.append
81 stringBuilder.append(serializeWithBuilder(name, stringBuilder));
82 // 添加一个冒号":"
83 stringBuilder.append(':');
84 // 添加序列化后的value,和之前一样,无须stringBuilder.append
85 stringBuilder.append(serializeWithBuilder(value, stringBuilder));
86 }
87 }
88 // 最后添加右括号
89 stringBuilder.append('}');
90 }
91 }
92 else { // 如果object为null
93 // 则添加null
94 stringBuilder.append('null');
95 }
96 break;
97
98 // 如果object为数字
99 case 'number':
100 // 如果表现一个有穷的值
101 if (isFinite(object)) {
102 // 添加该数字的字符串形式
103 stringBuilder.append(String(object));
104 }
105 else { // 如果是无穷的值
106 // 则添加"null"
107 stringBuilder.append('null');
108 }
109 break;
110 // 如果该对象为字符串
111 case 'string':
112 // 添加左双引号
113 stringBuilder.append('"');
114 var length = object.length;
115 // 枚举每一个字符
116 for (i = 0; i < length; ++i) {
117 var curChar = object.charAt(i);
118 if (curChar >= ' ') {
119 // 如果该字符为斜杠,或者双引号
120 // 则表明首先需要添加一个斜杠
121 if (curChar == '\\' || curChar == '"') {
122 stringBuilder.append('\\');
123 }
124 stringBuilder.append(curChar);
125 }
126 else {
127 // 处理特殊字符
128 switch (curChar) {
129 case '\b':
130 stringBuilder.append('\\b');
131 break;
132 case '\f':
133 stringBuilder.append('\\f');
134 break;
135 case '\n':
136 stringBuilder.append('\\n');
137 break;
138 case '\r':
139 stringBuilder.append('\\r');
140 break;
141 case '\t':
142 stringBuilder.append('\\t');
143 break;
144 default:
145 // 添加unicode字符
146 stringBuilder.append('\\u00');
147 stringBuilder.append(curChar.charCodeAt().toString(16));
148 }
149 }
150 }
151 // 添加右括号
152 stringBuilder.append('"');
153 break;
154
155 // 如果object是布尔值
156 case 'boolean':
157 // 则根据布尔值添加true或false
158 stringBuilder.append(object.toString());
159 break;
160
161 default:
162 stringBuilder.append('null');
163 break;
164 }
165 }
166 }
1 Sys.Serialization.JSON = new function() {
2
3 this.serialize = function(object) {
4 // 构造一个Sys.StringBuilder对象
5 var stringBuilder = new Sys.StringBuilder();
6 // 调用serializerWithBuilder方法序列化对象
7 serializeWithBuilder(object, stringBuilder);
8 // 返回结果
9 return stringBuilder.toString();
10 }
11
12 this.deserialize = function(data) {
13 // 简单地调用eval方法来反序列化对象
14 return eval('(' + data + ')');
15 }
16
17 function serializeWithBuilder(object, stringBuilder) {
18 var i;
19
20 // 检查传入的object类型
21 switch (typeof object) {
22 // 如果返回"object",表示该对象为Array或真正的Object
23 case 'object':
24 // 如果object不为null
25 if (object) {
26 // 如果该object为Array
27 if (Array.isInstanceOfType(object)) {
28 // 填写左方括号"["
29 stringBuilder.append('[');
30 // 枚举每一个对象
31 for (i = 0; i < object.length; ++i) {
32 // 如果不是第一个对象
33 if (i > 0) {
34 // 则添加一个逗号","
35 stringBuilder.append(',');
36 }
37 // 其实应该算作是一个Bug,
38 // 不应该是stringBuilder.append(...),
39 // 直接递归调用serializeWithBuilder方法即可,
40 // 不过stringBuilder.append(null)不影响结果
41 stringBuilder.append(serializeWithBuilder(object[i], stringBuilder));
42 }
43 // 添加右括号
44 stringBuilder.append(']');
45 }
46 else { // 如果是类型真正的Object
47 // 如果该对象有serialize函数
48 if (typeof object.serialize == 'function') {
49 // 则调用该方法获得序列化后的字符串
50 // 这是“自定义序列化”能力的关键!
51 stringBuilder.append(object.serialize());
52 break;
53 }
54 // 如果没有serialize函数
55 // 则使用默认序列化方式,添加左大括号
56 stringBuilder.append('{');
57 // 是否需要逗号的标记,初始化为false,表示第一个不需要
58 var needComma = false;
59 // 枚举每一个对象name
60 for (var name in object) {
61 // 如果由$开始(比如$index),则不序列化
62 if (name.startsWith('$')) {
63 continue;
64 }
65
66 // 获得值
67 var value = object[name];
68 // 如果该值不是undefined也不是一个函数
69 if (typeof value != 'undefined' && typeof value != 'function') {
70 // 如果需要逗号
71 if (needComma) {
72 // 则添加逗号
73 stringBuilder.append(',');
74 }
75 else {
76 // 第一个不需要逗号,则以后需要
77 needComma = true;
78 }
79
80 // 首先添加name,和之前一样,无须stringBuilder.append
81 stringBuilder.append(serializeWithBuilder(name, stringBuilder));
82 // 添加一个冒号":"
83 stringBuilder.append(':');
84 // 添加序列化后的value,和之前一样,无须stringBuilder.append
85 stringBuilder.append(serializeWithBuilder(value, stringBuilder));
86 }
87 }
88 // 最后添加右括号
89 stringBuilder.append('}');
90 }
91 }
92 else { // 如果object为null
93 // 则添加null
94 stringBuilder.append('null');
95 }
96 break;
97
98 // 如果object为数字
99 case 'number':
100 // 如果表现一个有穷的值
101 if (isFinite(object)) {
102 // 添加该数字的字符串形式
103 stringBuilder.append(String(object));
104 }
105 else { // 如果是无穷的值
106 // 则添加"null"
107 stringBuilder.append('null');
108 }
109 break;
110 // 如果该对象为字符串
111 case 'string':
112 // 添加左双引号
113 stringBuilder.append('"');
114 var length = object.length;
115 // 枚举每一个字符
116 for (i = 0; i < length; ++i) {
117 var curChar = object.charAt(i);
118 if (curChar >= ' ') {
119 // 如果该字符为斜杠,或者双引号
120 // 则表明首先需要添加一个斜杠
121 if (curChar == '\\' || curChar == '"') {
122 stringBuilder.append('\\');
123 }
124 stringBuilder.append(curChar);
125 }
126 else {
127 // 处理特殊字符
128 switch (curChar) {
129 case '\b':
130 stringBuilder.append('\\b');
131 break;
132 case '\f':
133 stringBuilder.append('\\f');
134 break;
135 case '\n':
136 stringBuilder.append('\\n');
137 break;
138 case '\r':
139 stringBuilder.append('\\r');
140 break;
141 case '\t':
142 stringBuilder.append('\\t');
143 break;
144 default:
145 // 添加unicode字符
146 stringBuilder.append('\\u00');
147 stringBuilder.append(curChar.charCodeAt().toString(16));
148 }
149 }
150 }
151 // 添加右括号
152 stringBuilder.append('"');
153 break;
154
155 // 如果object是布尔值
156 case 'boolean':
157 // 则根据布尔值添加true或false
158 stringBuilder.append(object.toString());
159 break;
160
161 default:
162 stringBuilder.append('null');
163 break;
164 }
165 }
166 }
这个方法其实写得非常的清晰和简单,我的注释其实更加像是画蛇添足。另外,这个方法完全可以剥离出来利用在任何需要的地方,即使不使用Atlas客户端代码,这也是一个值得被单独取出并加以使用的方法。
该方法中最关键的地方在于第47-53行,它表明了用户可以自定义一个对象的序列化方式:只要为这个对象添加一个serialize函数即可。这也就是在客户端支持互相引用对象序列化的方式,以后将配合服务器端的支持,通过实例来详细讲解这一点。
2、使用JavaScriptObjectDeserializer.DeserializedDictionary静态方法将字符串反序列画成一个Dictionary
这个静态方法的实现其实是将参数传入JavaScriptObjectDeserializer的构造函数,并调用该实例的方法DeserializeDictionary方法。因此我们直接来分析JavaScriptObjectDeserializer(int)实例方法。
DeserializeDictionary方法
1 private IDictionary<string, object> DeserializeDictionary(int depth)
2 {
3 IDictionary<string, object> dictResult = null;
4
5 char? currChar = this._s.MoveNext();
6 if ((currChar.GetValueOrDefault() != '{') || !currChar.HasValue)
7 {
8 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, '{' expected."));
9 }
10
11 while (true)
12 {
13 // 省略字符串序列化子对象之前的基本合法性验证代码
14 ……
15
16 // 子对象名
17 string name = null;
18 char? nextChar = currChar;
19
20 // 如果没有移至对象末尾标志,则进入反序列化当前对象内部数据
21 if ((nextChar.GetValueOrDefault() != '}') || !nextChar.HasValue)
22 {
23 // 得到对象名,就是key-value中的key
24 name = this.DeserializeMemberName();
25
26 // 如果不是合法的对象名,则抛出异常
27 if (string.IsNullOrEmpty(name))
28 {
29 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, member name expected."));
30 }
31
32 // 移至下一个非空字符
33 currChar = this._s.GetNextNonEmptyChar();
34 // 下一个如果不是冒号,则抛出异常
35 if ((currChar.GetValueOrDefault() != ':') || !currChar.HasValue)
36 {
37 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, ':' or '}' expected."));
38 }
39 }
40
41 if (dictResult == null)
42 {
43 // 构造即将返回的Dictionary对象
44 dictResult = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
45 // 如果没有name,说明已经反序列化至对象末尾了
46 if (string.IsNullOrEmpty(name))
47 {
48 // 移至下一个非空字符,为以后的反序列化子对象做准备
49 currChar = this._s.GetNextNonEmptyChar();
50 // 返回结果
51 return dictResult;
52 }
53 }
54
55 // 调用DeserializeIntenal方法获得即将反序列化的子对象
56 dictResult[name] = this.DeserializeInternal(depth);
57
58 // 序列化完了,移至下一个非空字符,为了准备下一个子对象的反序列化
59 currChar = this._s.GetNextNonEmptyChar();
60
61 // 如果是右大括号,表示对象内的子对象已经被序列化玩了
62 if ((currChar.GetValueOrDefault() == '}') && currChar.HasValue)
63 {
64 // 则返回结果
65 return dictResult;
66 }
67
68 // 如果不是逗号,则说明子对象之间没有分隔符,抛出异常
69 if ((currChar.GetValueOrDefault() != ',') || !currChar.HasValue)
70 {
71 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, ',' or '}' expected."));
72 }
73 }
74 }
1 private IDictionary<string, object> DeserializeDictionary(int depth)
2 {
3 IDictionary<string, object> dictResult = null;
4
5 char? currChar = this._s.MoveNext();
6 if ((currChar.GetValueOrDefault() != '{') || !currChar.HasValue)
7 {
8 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, '{' expected."));
9 }
10
11 while (true)
12 {
13 // 省略字符串序列化子对象之前的基本合法性验证代码
14 ……
15
16 // 子对象名
17 string name = null;
18 char? nextChar = currChar;
19
20 // 如果没有移至对象末尾标志,则进入反序列化当前对象内部数据
21 if ((nextChar.GetValueOrDefault() != '}') || !nextChar.HasValue)
22 {
23 // 得到对象名,就是key-value中的key
24 name = this.DeserializeMemberName();
25
26 // 如果不是合法的对象名,则抛出异常
27 if (string.IsNullOrEmpty(name))
28 {
29 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, member name expected."));
30 }
31
32 // 移至下一个非空字符
33 currChar = this._s.GetNextNonEmptyChar();
34 // 下一个如果不是冒号,则抛出异常
35 if ((currChar.GetValueOrDefault() != ':') || !currChar.HasValue)
36 {
37 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, ':' or '}' expected."));
38 }
39 }
40
41 if (dictResult == null)
42 {
43 // 构造即将返回的Dictionary对象
44 dictResult = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
45 // 如果没有name,说明已经反序列化至对象末尾了
46 if (string.IsNullOrEmpty(name))
47 {
48 // 移至下一个非空字符,为以后的反序列化子对象做准备
49 currChar = this._s.GetNextNonEmptyChar();
50 // 返回结果
51 return dictResult;
52 }
53 }
54
55 // 调用DeserializeIntenal方法获得即将反序列化的子对象
56 dictResult[name] = this.DeserializeInternal(depth);
57
58 // 序列化完了,移至下一个非空字符,为了准备下一个子对象的反序列化
59 currChar = this._s.GetNextNonEmptyChar();
60
61 // 如果是右大括号,表示对象内的子对象已经被序列化玩了
62 if ((currChar.GetValueOrDefault() == '}') && currChar.HasValue)
63 {
64 // 则返回结果
65 return dictResult;
66 }
67
68 // 如果不是逗号,则说明子对象之间没有分隔符,抛出异常
69 if ((currChar.GetValueOrDefault() != ',') || !currChar.HasValue)
70 {
71 throw new ArgumentException(this._s.GetDebugString("Invalid object passed in, ',' or '}' expected."));
72 }
73 }
74 }
在这个方法中可以看到一个实例变量_s的使用。它是一个Microsoft.Web.Script.Serialization.JavaScriptString类型的辅助类,封装了一个字符串(就是这里需要进行反序列化操作的字符串),并提供了在反序列化过程中所需的方法(例如GetNextNonEmptyChar),从这些方法的名字内可以就可以明确地看出他们的作用,因此就不作详细分析了。它们的实现,其实也就是对字符串进行分析的过程。
在这里还出现了一个非常重要的方法就是DeserializeInternal。它虽然简单,但是负责了对于反序列化函数的选择,并最终获得反序列化后的对象。我们来看一下它的代码:
DeserializeInternal方法
1 private object DeserializeInternal(int depth)
2 {
3 // 如果已经超出的深度限制,则抛出异常
4 if (++depth > this._depthLimit)
5 {
6 throw new ArgumentException(this._s.GetDebugString(AtlasWeb.DepthLimitExceeded));
7 }
8
9 // 获得下一个非空字符
10 char? nextChar = this._s.GetNextNonEmptyChar();
11 // 如果无法获得,则返回null
12 if (!nextChar.HasValue)
13 {
14 return null;
15 }
16
17 // 移回至前一个字符,因为序列化要从最起初开始,
18 // 例如序列化下一个IDictionary对象时当前字符必须
19 // 在"{"之前,序列化IList对象时必须在"["之前等。
20 this._s.MovePrev();
21
22 // 如果下一个元素是客户端的元素,
23 // 即检测是不是以"new"开头。
24 if (this.IsNextElementClientObject())
25 {
26 // 则反序列化客户端元素,在目前即是返回一个DateTime对象,
27 // 也就是说,是识别new Date(...)这样的字符串。
28 return this.DeserializeStringIntoClientObject();
29 }
30
31 // 如果下一个元素是一个对象(判断nextChar是否为"{")
32 if (JavaScriptObjectDeserializer.IsNextElementObject(nextChar))
33 {
34 // 则调用反序列化Dictionary的方法返回对象
35 return this.DeserializeDictionary(depth);
36 }
37
38 // 如果下一个元素是Array(判断nextChar是否为"[")
39 if (JavaScriptObjectDeserializer.IsNextElementArray(nextChar))
40 {
41 // 则调用反序列化List的方法返回对象
42 return this.DeserializeList(depth);
43 }
44
45 // 如果下一个元素是String(判断下一个对象是否为双引号)
46 if (JavaScriptObjectDeserializer.IsNextElementString(nextChar))
47 {
48 // 则调用反序列化字符串的方法返回对象
49 return this.DeserializeString();
50 }
51
52 // 如果执行到了这一步,则说明下一个元素是一个基本类型的值了
53 // 下面这个方法会返回null, 布尔值,Integer或Double
54 return this.DeserializePrimitiveObject();
55 }
1 private object DeserializeInternal(int depth)
2 {
3 // 如果已经超出的深度限制,则抛出异常
4 if (++depth > this._depthLimit)
5 {
6 throw new ArgumentException(this._s.GetDebugString(AtlasWeb.DepthLimitExceeded));
7 }
8
9 // 获得下一个非空字符
10 char? nextChar = this._s.GetNextNonEmptyChar();
11 // 如果无法获得,则返回null
12 if (!nextChar.HasValue)
13 {
14 return null;
15 }
16
17 // 移回至前一个字符,因为序列化要从最起初开始,
18 // 例如序列化下一个IDictionary对象时当前字符必须
19 // 在"{"之前,序列化IList对象时必须在"["之前等。
20 this._s.MovePrev();
21
22 // 如果下一个元素是客户端的元素,
23 // 即检测是不是以"new"开头。
24 if (this.IsNextElementClientObject())
25 {
26 // 则反序列化客户端元素,在目前即是返回一个DateTime对象,
27 // 也就是说,是识别new Date(...)这样的字符串。
28 return this.DeserializeStringIntoClientObject();
29 }
30
31 // 如果下一个元素是一个对象(判断nextChar是否为"{")
32 if (JavaScriptObjectDeserializer.IsNextElementObject(nextChar))
33 {
34 // 则调用反序列化Dictionary的方法返回对象
35 return this.DeserializeDictionary(depth);
36 }
37
38 // 如果下一个元素是Array(判断nextChar是否为"[")
39 if (JavaScriptObjectDeserializer.IsNextElementArray(nextChar))
40 {
41 // 则调用反序列化List的方法返回对象
42 return this.DeserializeList(depth);
43 }
44
45 // 如果下一个元素是String(判断下一个对象是否为双引号)
46 if (JavaScriptObjectDeserializer.IsNextElementString(nextChar))
47 {
48 // 则调用反序列化字符串的方法返回对象
49 return this.DeserializeString();
50 }
51
52 // 如果执行到了这一步,则说明下一个元素是一个基本类型的值了
53 // 下面这个方法会返回null, 布尔值,Integer或Double
54 return this.DeserializePrimitiveObject();
55 }
这是个很标准的间接递归函数,非常容易理解:通过判断即将序列化的那个对象的类型来选择反序列化下一个对象的方法。而那些方法之中又会调用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方法调用中使用复杂类型的核心之一。
那么它究竟是如何工作的?我们将在下一篇文章中讨论这个问题。
这篇其实还没有谈到解决方案,下一篇会涉及到一点。不过可能真正的范例需要更后面才能谈到,喜欢看范例的朋友们只能多多包涵了。
关于Atlas支持复杂对象的方法还是很有看头,我分析了Atlas实现,然后得出一些解决方案,但是可能有的朋友能够相处更好的办法,所以我希望能够和大家多多交流Atlas的相关经验:)
//在写的时候好像园子变得非常慢,还保存失败过一次……不过现在似乎已经好了。