通过WCF基本的异常处理模式[上篇], 我们知道了:在默认的情况下,服务端在执行某个服务操作时抛出的异常(在这里指非FaultException异常),其相关的错误信息仅仅限于服务端可见,并不会被WCF传递到客户端;如果将开启了IncludeExceptionDetailInFaults的ServiceDebug服务行为通过声明(通过在服务类型上应用ServiceBehaviorAttrite特性)或者配置的方式应用到相应的服务上,异常相关的所有细节信息将会原封不动地向客户端传送。
这两种方式体现了两种极端的异常传播(Exception Propagation)机制,对于基于服务操作执行过程中抛出的异常的错误细节,要么完全对客户端屏蔽,要么全部暴露于客户端。在真正通过WCF来架构我们的分布式系统中,我们往往需要一种折中的异常传播机制:自定义服务端异常信息。这样既可以让客户端得到一个易于理解的错误信息,又在一定程度上避免了一些敏感信息的泄露。
一、 通过FaultException直接指定错误信息
对于执行服务操作中抛出的异常,如果服务的定义者仅仅希望服务的调用者得到一段自定义的错误信息文本(字符串),我们要做的实际上很简单:在服务操作中直接抛出一个FaultException异常,该异常对象通过以字符串形式体现的自定义错误信息创建。下面的代码中,CalculaorService的Divide方式在指定的时候对第二参数进行了验证,如果为零则创建一个FaultException,并指定错误信息(“被除数y不能为零!”)。
1: using System.ServiceModel;<!--CRLF-->
2: using Artech.WcfServices.Contracts;<!--CRLF-->
3: namespace Artech.WcfServices.Services<!--CRLF-->
4: {
<!--CRLF-->
5: [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
<!--CRLF-->
6: public class CalculatorService : ICalculator<!--CRLF-->
7: {
<!--CRLF-->
8: public int Divide(int x, int y)<!--CRLF-->
9: {
<!--CRLF-->
10: if (0 == y)<!--CRLF-->
11: {
<!--CRLF-->
12: throw new FaultException("被除数y不能为零!");<!--CRLF-->
13: }
<!--CRLF-->
14: return x / y;<!--CRLF-->
15: }
<!--CRLF-->
16: }
<!--CRLF-->
17: }
<!--CRLF-->
客户端在调用该服务操作的时候,如果传入零作为被除数,将会直接捕获服务端定义的抛出的这个异常(实际上,这其中经历了异常对象的序列化、消息交换以及异常对象的反序列化等一系列的操作)。客户端具体的异常捕获情况如下面的程序体现:
1: using System;<!--CRLF-->
2: using System.ServiceModel;<!--CRLF-->
3: using Artech.WcfServices.Contracts;<!--CRLF-->
4: namespace Artech.WcfServices.Clients<!--CRLF-->
5: {
<!--CRLF-->
6: class Program<!--CRLF-->
7: {
<!--CRLF-->
8: static void Main(string[] args)<!--CRLF-->
9: {
<!--CRLF-->
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(<!--CRLF-->
11: "calculatorservice"))<!--CRLF-->
12: {
<!--CRLF-->
13: ICalculator calculator = channelFactory.CreateChannel();
<!--CRLF-->
14: using (calculator as IDisposable)<!--CRLF-->
15: {
<!--CRLF-->
16: try<!--CRLF-->
17: {
<!--CRLF-->
18: int result = calculator.Divide(1, 0);<!--CRLF-->
19: }
<!--CRLF-->
20: catch (FaultException ex)<!--CRLF-->
21: {
<!--CRLF-->
22: Console.WriteLine(ex.Message);
<!--CRLF-->
23: (calculator as ICommunicationObject).Abort();<!--CRLF-->
24: }
<!--CRLF-->
25: }
<!--CRLF-->
26: }
<!--CRLF-->
27: }
<!--CRLF-->
28: }
<!--CRLF-->
29: }
<!--CRLF-->
输出结果:
被除数y不能为零!
虽然在很多情况下,在服务端指定服务操作的过程中直接抛出含有自定义错误信息的FaultException异常,就能过客户端感知到遇到的具体错误并进行必要的排错和纠错。但是,我们更多地,还是倾向于直接定义一个类型来描述异常信息。我个人倾向于这样一类的类型为错误明细类型(Fault Detail Type)。服务端根据具体的异常场景创建相应的错误类型对象,并基于该对象我们上面提到的System.ServiceModel.FaultException<TDetail>异常,其中泛型类型参数为异常细节类型。在这个过程中,还涉及到一个重要的概念:错误契约(Fault Contract),接下来,我们就来介绍一下FaultException<TDetail>和错误契约。
二、 通过FaultException<TDetail>采用自定义类型封装错误
由于用于封装错误信息的异常细节类型的对象最终需要通过消息交换的方式从服务端传播到客户端,所以该对象必须是一个可序列化的对象。WCF通过两种典型序列化器实现对数据对象的序列化和反序列化,其中一个是传统的System.Xml.Serialization.XmlSerializer,该序列换器被ASP.NET Web服务用于对象和XML之间的序列化和反序列化;另一个则是System.Runtime.Serialization.DataContractSerializer,用于基于数据契约对象的序列化和反序列化,后者是WCF默认采用的序列化器。所以,可序列化的错误明细类型一般是一个数据契约,或者是一个应用了System.SerializableAttribute特性的类型。关于序列化,和与此相关的数据契约、数据契约序列化器等,在《WCF技术剖析(卷1)》的第5章有深入、全面的介绍。
我们仍然用我们上面提到的计算服务来举例,现在我们需要定义一个独立的类型来描述基于CalculatorService的异常,我们索性将该类型起名为CalculationError。我们将CalculationError定义成一个应用了System.Runtime.Serialization.DataContractAttribute特性的数据契约,简单起见,我们仅仅定义了两个数据成员(应用了System.Runtime.Serialization.DataMemberAttribute特性):Operation表示导致异常相应的运算操作(我们假设CalculatorService具有一系列运算操作,虽然我们的例子中仅仅给出为一一个除法运算操作:Divide),而Message表述具体的错误消息。CalculationError的定义在被客户端(Client项目)和服务(Services项目)引用的契约(Contracts项目)中,具体定义如下:
1: using System;<!--CRLF-->
2: using System.Runtime.Serialization;<!--CRLF-->
3: namespace Artech.WcfServices.Contracts<!--CRLF-->
4: {
<!--CRLF-->
5: [DataContractAttribute(Namespace="http://www.artech.com/")]<!--CRLF-->
6: public class CalculationError<!--CRLF-->
7: {
<!--CRLF-->
8: public CalculationError(string operation,string message)<!--CRLF-->
9: {
<!--CRLF-->
10: if (string.IsNullOrEmpty(operation))<!--CRLF-->
11: {
<!--CRLF-->
12: throw new ArgumentNullException("operation");<!--CRLF-->
13: }
<!--CRLF-->
14: if (string.IsNullOrEmpty(message))<!--CRLF-->
15: {
<!--CRLF-->
16: throw new ArgumentNullException("message");<!--CRLF-->
17: }
<!--CRLF-->
18: this.Operation = operation;<!--CRLF-->
19: this.Message = message;<!--CRLF-->
20: }
<!--CRLF-->
21: [DataMember]
<!--CRLF-->
22: public string Operation<!--CRLF-->
23: { get; set; }
<!--CRLF-->
24: [DataMember]
<!--CRLF-->
25: public string Message<!--CRLF-->
26: { get; set; }
<!--CRLF-->
27: }
<!--CRLF-->
28: }
<!--CRLF-->
照理说,我们已经正确定义了错误明细类型CalculationError,在CalculatorService的Divide操作中就可以直接抛出一个Fault<CalculationError>,并将一个创建一个CalculationError对象作为该异常对象的明细(通过Detail属性表示),具体的代码如下所示:
1: using System.ServiceModel;<!--CRLF-->
2: using Artech.WcfServices.Contracts;<!--CRLF-->
3: namespace Artech.WcfServices.Services<!--CRLF-->
4: {
<!--CRLF-->
5: public class CalculatorService : ICalculator<!--CRLF-->
6: {
<!--CRLF-->
7: public int Divide(int x, int y)<!--CRLF-->
8: {
<!--CRLF-->
9: if (0 == y)<!--CRLF-->
10: {
<!--CRLF-->
11: var error = new CalculationError("Divide", "被除数y不能为零!");<!--CRLF-->
12: throw new FaultException<CalculationError>(error,error.Message);<!--CRLF-->
13: }
<!--CRLF-->
14: return x / y;<!--CRLF-->
15: }
<!--CRLF-->
16: }
<!--CRLF-->
17: }
<!--CRLF-->
客户端服务调用相关的异常处理也作如下相应的修改:
1: using System;<!--CRLF-->
2: using System.ServiceModel;<!--CRLF-->
3: using Artech.WcfServices.Contracts;<!--CRLF-->
4: namespace Artech.WcfServices.Clients<!--CRLF-->
5: {
<!--CRLF-->
6: class Program<!--CRLF-->
7: {
<!--CRLF-->
8: static void Main(string[] args)<!--CRLF-->
9: {
<!--CRLF-->
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(<!--CRLF-->
11: "calculatorservice"))<!--CRLF-->
12: {
<!--CRLF-->
13: ICalculator calculator = channelFactory.CreateChannel();
<!--CRLF-->
14: using (calculator as IDisposable)<!--CRLF-->
15: {
<!--CRLF-->
16: try<!--CRLF-->
17: {
<!--CRLF-->
18: int result = calculator.Divide(1, 0);<!--CRLF-->
19: }
<!--CRLF-->
20: catch (FaultException<CalculationError> ex)<!--CRLF-->
21: {
<!--CRLF-->
22: Console.WriteLine("运算错误");<!--CRLF-->
23: Console.WriteLine("运算操作:{0}",ex.Detail.Operation);<!--CRLF-->
24: Console.WriteLine("错误消息:{0}",ex.Detail.Message);<!--CRLF-->
25: (calculator as ICommunicationObject).Abort();<!--CRLF-->
26: }
<!--CRLF-->
27: }
<!--CRLF-->
28: }
<!--CRLF-->
29: }
<!--CRLF-->
30: }
<!--CRLF-->
31: }
<!--CRLF-->
但是我们的客户端程序并不能按照我们实现预想的那样正常运行,而会抛出如图1所示的未被处理的FaultException异常,而我们试图捕获的异常类型为FaultException<CalculationError>。
图1 客户端不能正常捕获FaultException<CalculationError>异常
三、错误契约(Fault Contract)
要回答上面出错的原因,就需要谈到WCF或者SOA一个根本的特征:服务的提供者和消费者之间采用基于“契约(Contract)”的交互方式。不同于面向服务,在面向组件设计中,组件之间的交互实际上是基于类型的,交互双方需要共享相同类型集(接口、抽象类或者具体类等)。在《WCF技术剖析(卷1)》中,我们曾多次对契约进行过深入的探讨。从抽象层面上讲,契约时交互双方或者多方就某一问题达成的一种共识,使确保正常交互指定的一系列的规范。
从本质上讲,服务契约(Service Contract)中的每一个操作契约(Operation Contract),定义了WCF为实现该服务操作的调用采用的消息交换模式(MEP:Message Exchange Pattern),并且结合基于参数、返回值类型的数据契约、消息契约定义了请求消息和回复消息的结构(Schema)。数据契约建立了对相同数据的两种不同表现形式(托管对象和XML)之间的双向适配,以利于承载相同信息的数据在两种不同形态之间的转换,即序列换和反序列化。而消息契约在定义了托管对象的各个成员与相应的消息元素之间的匹配关系。借助于消息契约,在对一个托管对象进行序列化并生成消息的时候,可以有效地控制某一个数据成员(属性或者字段)被序列化成的XML应该置于消息报头(Header)还是消息主体(Body)。
总的来说,上述的这些契约基本上都是围绕着一个正常服务调用下的消息交换:服务的消费者通过向服务的提供者发送请求消息,服务的提供者在接受到该请求后,激活服务实例并调用相应的服务操作,最终将返回的结果以回复消息的方式返回给服务的消费者(对于One-way,则不需要消息的回复)。但是,如果服务操作不能正确地执行,服务端将会通过一种特殊的消息将错误信息返回给客户端,这种消息被称为错误消息(Fault Message)。对于错误消息,同样需要相应的契约来定义其结构,我们把这种契约称为错误契约(Fault Contract)。
WCF通过System.ServiceModel.FaultContractAttribute特性定义,由于错误契约是基于服务操作级别的,所以该特性直接应用于服务契约接口或者类的操作契约方法成员上。下面的代码片断体现了FaultContractAttribute的定义:
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]<!--CRLF-->
2: public sealed class FaultContractAttribute : Attribute<!--CRLF-->
3: {
<!--CRLF-->
4: public FaultContractAttribute(Type detailType);<!--CRLF-->
5: public string Action { get; set; }<!--CRLF-->
6: public Type DetailType { get; }<!--CRLF-->
7: public bool HasProtectionLevel { get; }<!--CRLF-->
8: public string Name { get; set; }<!--CRLF-->
9: public string Namespace { get; set; }<!--CRLF-->
10: public ProtectionLevel ProtectionLevel { get; set; }<!--CRLF-->
11: }
<!--CRLF-->
FaultContractAttribute的6个属性分别具有如下的含义:
下面的代码中,我们通过FaultContractAttribute将我们定义错误明细类型CalculationError应用到Divide操作之上,这样的话,我们的例子就能够正常运行了。
评论