WCF技术剖析之二十一:WCF基本异常处理模式[中篇]

通过WCF基本的异常处理模式[上篇], 我们知道了:在默认的情况下,服务端在执行某个服务操作时抛出的异常(在这里指非FaultException异常),其相关的错误信息仅仅限于服务端可见,并不会被WCF传递到客户端;如果将开启了IncludeExceptionDetailInFaults的ServiceDebug服务行为通过声明(通过在服务类型上应用ServiceBehaviorAttrite特性)或者配置的方式应用到相应的服务上,异常相关的所有细节信息将会原封不动地向客户端传送。

这两种方式体现了两种极端的异常传播(Exception Propagation)机制,对于基于服务操作执行过程中抛出的异常的错误细节,要么完全对客户端屏蔽,要么全部暴露于客户端。在真正通过WCF来架构我们的分布式系统中,我们往往需要一种折中的异常传播机制:自定义服务端异常信息。这样既可以让客户端得到一个易于理解的错误信息,又在一定程度上避免了一些敏感信息的泄露。

一、 通过FaultException直接指定错误信息

对于执行服务操作中抛出的异常,如果服务的定义者仅仅希望服务的调用者得到一段自定义的错误信息文本(字符串),我们要做的实际上很简单:在服务操作中直接抛出一个FaultException异常,该异常对象通过以字符串形式体现的自定义错误信息创建。下面的代码中,CalculaorService的Divide方式在指定的时候对第二参数进行了验证,如果为零则创建一个FaultException,并指定错误信息(“被除数y不能为零!”)。

   1: using System.ServiceModel;
   2: using Artech.WcfServices.Contracts;
   3: namespace Artech.WcfServices.Services
   4: {
   5:     [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
   6:     public class CalculatorService : ICalculator
   7:     {
   8:         public int Divide(int x, int y)
   9:         {
  10:             if (0 == y)
  11:             {
  12:                 throw new FaultException("被除数y不能为零!");
  13:             }
  14:             return x / y;
  15:         }
  16:     }
  17: }

客户端在调用该服务操作的时候,如果传入零作为被除数,将会直接捕获服务端定义的抛出的这个异常(实际上,这其中经历了异常对象的序列化、消息交换以及异常对象的反序列化等一系列的操作)。客户端具体的异常捕获情况如下面的程序体现:

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.WcfServices.Contracts;
   4: namespace Artech.WcfServices.Clients
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
  11:                "calculatorservice"))
  12:             {
  13:                 ICalculator calculator = channelFactory.CreateChannel();
  14:                 using (calculator as IDisposable)
  15:                 {
  16:                     try
  17:                     {
  18:                         int result = calculator.Divide(1, 0);
  19:                     }
  20:                     catch (FaultException ex)
  21:                     {
  22:                         Console.WriteLine(ex.Message);
  23:                         (calculator as ICommunicationObject).Abort();
  24:                     }
  25:                 }
  26:             }
  27:         }
  28:     }
  29: }

输出结果:

被除数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;
   2: using System.Runtime.Serialization;
   3: namespace Artech.WcfServices.Contracts
   4: {
   5:     [DataContractAttribute(Namespace="http://www.artech.com/")]
   6:     public class CalculationError
   7:     {
   8:         public CalculationError(string operation,string message)
   9:         {
  10:             if (string.IsNullOrEmpty(operation))
  11:             {
  12:                 throw new ArgumentNullException("operation");
  13:             }
  14:             if (string.IsNullOrEmpty(message))
  15:             {
  16:                 throw new ArgumentNullException("message");
  17:             }
  18:             this.Operation = operation;
  19:             this.Message = message;
  20:         }
  21:         [DataMember]
  22:         public string Operation
  23:         { get; set; }
  24:         [DataMember]
  25:         public string Message
  26:         { get; set; }
  27:     }
  28: }

照理说,我们已经正确定义了错误明细类型CalculationError,在CalculatorService的Divide操作中就可以直接抛出一个Fault<CalculationError>,并将一个创建一个CalculationError对象作为该异常对象的明细(通过Detail属性表示),具体的代码如下所示:

   1: using System.ServiceModel;
   2: using Artech.WcfServices.Contracts;
   3: namespace Artech.WcfServices.Services
   4: {
   5:     public class CalculatorService : ICalculator
   6:     {
   7:         public int Divide(int x, int y)
   8:         {
   9:             if (0 == y)
  10:             {
  11:                 var error = new CalculationError("Divide", "被除数y不能为零!");
  12:                 throw new FaultException<CalculationError>(error,error.Message);
  13:             }
  14:             return x / y;
  15:         }
  16:     }
  17: }

客户端服务调用相关的异常处理也作如下相应的修改:

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.WcfServices.Contracts;
   4: namespace Artech.WcfServices.Clients
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
  11:                "calculatorservice"))
  12:             {
  13:                 ICalculator calculator = channelFactory.CreateChannel();
  14:                 using (calculator as IDisposable)
  15:                 {
  16:                     try
  17:                     {
  18:                         int result = calculator.Divide(1, 0);
  19:                     }
  20:                     catch (FaultException<CalculationError> ex)
  21:                     {
  22:                         Console.WriteLine("运算错误");
  23:                         Console.WriteLine("运算操作:{0}",ex.Detail.Operation);
  24:                         Console.WriteLine("错误消息:{0}",ex.Detail.Message);
  25:                         (calculator as ICommunicationObject).Abort();
  26:                     }
  27:                 }
  28:             }
  29:         }
  30:     }
  31: }

但是我们的客户端程序并不能按照我们实现预想的那样正常运行,而会抛出如图1所示的未被处理的FaultException异常,而我们试图捕获的异常类型为FaultException<CalculationError>。

clip_image002

图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)]
   2: public sealed class FaultContractAttribute : Attribute
   3: {    
   4:     public FaultContractAttribute(Type detailType);
   5:     public string Action { get; set; }
   6:     public Type DetailType { get; }
   7:     public bool HasProtectionLevel { get; }
   8:     public string Name { get; set; }
   9:     public string Namespace { get; set; }
  10:     public ProtectionLevel ProtectionLevel { get; set; }
  11: }

FaultContractAttribute的6个属性分别具有如下的含义:

  • Action:和一般的SOAP消息一样,对于Fault SOAP,WS-Address报头Action是必须的,该属性控制Action报头的值。如果Action属性没有在应用FaultContractAttribute时显式指定,那么它将按照下面的规则进行指定:{服务契约命名空间}/{服务契约名称}/{操作契约名称}{明细类型名称}Fault;
  • DetailType也就是上面所介绍的用于封装错误信息的错误明细类型,比如我们前面定义的CalculationError;
  • Name和Namespace:在最终的Fault SOAP中,错误明细对象被序列化成的XML将会被置于Fault SOAP的主体部分,而这两个属性则用于控制这段XML片断对应的名称和命名空间;如果这两个属性并未作显式设置,WCF将会使用DetailType对应的数据契约名称和命名空间;
  • HasProtectionLevel和ProtectionLevel:这两个属性涉及到保护级别,属于安全(Security)的问题,在这里就不多作介绍了。

下面的代码中,我们通过FaultContractAttribute将我们定义错误明细类型CalculationError应用到Divide操作之上,这样的话,我们的例子就能够正常运行了。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {
   4:     [ServiceContract(Namespace = "http://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(CalculationError))]
   9:         int Divide(int x, int y);
  10:     }   
  11: }

按照我们在上面提到的关于Action、Name和Namespace的默认设定,上面这段代码和下面的代码是完全等效的。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {
   4:     [ServiceContract(Name="ICalculator",Namespace = "http://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract(Name="Divide")]
   8:         [FaultContract(typeof(CalculationError), 
   9:             Action = "http://www.artech.com/ICalculator/DivideCalculationErrorFault",
  10:             Name = "CalculationError", Namespace = "http://www.artech.com/")]
  11:         int Divide(int x, int y);
  12:     }  
  13: }

对于我们前面的例子,当客户端调用CalculatorService的Divide操作执行除法预算,并传入零作为被除数,服务端将会抛出FaultException<CalculationError>异常。WCF服务端框架将会产生一个Fault Message,并将序列化后的CalculationError对象作为错误明细放置到Fault Message的主体部分。如果采用的消息版本是Soap12Addressing10,即SOAP 1.2和WS-Addressing 1.0,最终生成的Fault Message将会如下面的XML片断所示:

   1: <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
   2:   <s:Header>
   3:     <a:Action s:mustUnderstand="1">http://www.artech.com/ICalculator/DivideCalculationErrorFault</a:Action>    <a:RelatesTo>urn:uuid:3498ba2d-edd0-4d3b-ba4a-9b35327b5fa3</a:RelatesTo>
   4:   </s:Header>
   5:   <s:Body>
   6:     <s:Fault>
   7:       <s:Code>
   8:         <s:Value>s:Sender</s:Value>
   9:       </s:Code>
  10:       <s:Reason>
  11:         <s:Text xml:lang="zh-CN">被除数y不能为零!</s:Text>
  12:       </s:Reason>
  13:       <s:Detail>
  14:         <CalculationError xmlns="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  15:           <Message>被除数y不能为零!</Message>
  16:           <Operation>Divide</Operation>
  17:         </CalculationError>
  18:       </s:Detail>
  19:     </s:Fault>
  20:   </s:Body>
  21: </s:Envelope>

错误契约作为服务描述的一部分,会参与到描述服务的元数据(Metadata)中。当服务元数据通过WSDL的形式被发布的时候,作为对操作的描述的错误契约体现在WSDL的<wsdl:portType>/<wsdl:operation>/<wsdl:fault>节点。下面一段XML代表CalculatorService的WDSL:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <wsdl:definitions name="CalculatorService" targetNamespace="http://www.artech.com/" >
   3:   <wsdl:import namespace="http://tempuri.org/" location="http://127.0.0.1:3721/calculatorservice/mex?wsdl=wsdl0"/>
   4:   <wsdl:types>
   5:     <xs:schema elementFormDefault="qualified" targetNamespace="http://www.artech.com/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.artech.com/">
   6:       <xs:element name="Divide">
   7:         <xs:complexType>
   8:           <xs:sequence>
   9:             <xs:element minOccurs="0" name="x" type="xs:int"/>
  10:             <xs:element minOccurs="0" name="y" type="xs:int"/>
  11:           </xs:sequence>
  12:         </xs:complexType>
  13:       </xs:element>
  14:       <xs:element name="DivideResponse">
  15:         <xs:complexType>
  16:           <xs:sequence>
  17:             <xs:element minOccurs="0" name="DivideResult" type="xs:int"/>
  18:           </xs:sequence>
  19:         </xs:complexType>
  20:       </xs:element>
  21:       <xs:complexType name="CalculationError">
  22:         <xs:sequence>
  23:           <xs:element minOccurs="0" name="Message" nillable="true" type="xs:string"/>
  24:           <xs:element minOccurs="0" name="Operation" nillable="true" type="xs:string"/>
  25:         </xs:sequence>
  26:       </xs:complexType>
  27:       <xs:element name="CalculationError" nillable="true" type="tns:CalculationError"/>
  28:     </xs:schema>
  29:   </wsdl:types>
  30:   <wsdl:message name="ICalculator_Divide_InputMessage">
  31:     <wsdl:part name="parameters" element="tns:Divide"/>
  32:   </wsdl:message>
  33:   <wsdl:message name="ICalculator_Divide_OutputMessage">
  34:     <wsdl:part name="parameters" element="tns:DivideResponse"/>
  35:   </wsdl:message>
  36:   <wsdl:message name="ICalculator_Divide_CalculationErrorFault_FaultMessage">
  37:     <wsdl:part name="detail" element="tns:CalculationError"/>
  38:   </wsdl:message>
  39:   <wsdl:portType name="ICalculator">
  40:     <wsdl:operation name="Divide">
  41:       <wsdl:input wsaw:Action="http://www.artech.com/ICalculator/Divide" message="tns:ICalculator_Divide_InputMessage"/>
  42:       <wsdl:output wsaw:Action="http://www.artech.com/ICalculator/DivideResponse" message="tns:ICalculator_Divide_OutputMessage"/>
  43:       <wsdl:fault wsaw:Action="http://www.artech.com/ICalculator/DivideCalculationErrorFault" name="CalculationErrorFault" message="tns:ICalculator_Divide_CalculationErrorFault_FaultMessage"/>
  44:     </wsdl:operation>
  45:   </wsdl:portType>
  46:   <wsdl:service name="CalculatorService">
  47:     <wsdl:port name="CustomBinding_ICalculator" binding="i0:CustomBinding_ICalculator">
  48:       <soap12:address location="http://127.0.0.1:3721/calculatorservice"/>
  49:       <wsa10:EndpointReference>        <wsa10:Address>http://127.0.0.1:3721/calculatorservice</wsa10:Address>
  50:       </wsa10:EndpointReference>
  51:     </wsdl:port>
  52:   </wsdl:service>
  53: </wsdl:definitions>

对于错误契约的应用,还有一点需要特别说明:不仅仅是将自定义的错误明细类型(比如CalculationError)应用到相应的操作契约上,你需要显失地利用FaultContractAttribute特性将其应用到服务契约接口或者类中相应的操作方法上面,对于一些基元类型,比如Int32,String等,你也需要这样。也即是说,同样对于我们的计算服务的例子,如果服务端试图通过抛出一个FaultException<string>来提供错误(如下面的代码所示),客户端最后捕获到的仅仅是一个FaultException异常,而非FaultException<string>异常。

   1: using System.ServiceModel;
   2: using Artech.WcfServices.Contracts;
   3: namespace Artech.WcfServices.Services
   4: {
   5:     public class CalculatorService : ICalculator
   6:     {
   7:         public int Divide(int x, int y)
   8:         {
   9:             if (0 == y)
  10:             {
  11:                 throw new FaultException<string>("被除数y不能为零!", "被除数y不能为零!");
  12:             }
  13:             return x / y;
  14:         }
  15: }
  16: }

在这种情况下,你需要做的仍然是在相应的操作上面,通过应用FaultContractAttribute特性指定String类型作为其DetailType,如下面的代码所示:

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {
   4:     [ServiceContract(Namespace = "http://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         [FaultContract(typeof(string))]
   9:         int Divide(int x, int y);
  10:     }   
  11: }

从FaultContractAttribute的定义我们可以看出,该特性可以在同一个目标对象上面多次应用(AllowMultiple = true)。这也很好理解:对于同一个服务操作,可能具有不同的异常场景,在不同的情况下,需要抛出不同的异常。

   1: [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
   2: public sealed class FaultContractAttribute : Attribute
   3: {    
   4:     //省略成员
   5: }

但是,如果你在同一个操作方法上面应用了多了FaultContractAttribute特性的时候,需要遵循一系列的规则,我们将在《WCF基本异常处理模式(下篇)》中进行逐条介绍。

作者:Artech
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2009-10-22 18:17 Artech 阅读(...) 评论(...) 编辑 收藏