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

由于WCF采用.NET托管语言(C#和NET)作为其主要的编程语言,注定以了基于WCF的编程方式不可能很复杂。同时,WCF设计的一个目的就是提供基于非业务逻辑的通信实现,为编程人员提供一套简单易用的应用编程接口(API)。WCF编程模式的简单性同样体现在异常处理上面,本篇文章的主要目的就是对WCF基于异常处理的编程模式做一个简单的介绍。

一、当异常从服务端抛出

对于一个典型的WCF服务调用,我个人倾向于将潜在抛出的异常费为两种类型:应用异常(Application Exception)和基础结构(Infrastructure Exception)。前者为应用级别,主要体现为执行某个服务操作的业务逻辑抛出的异常;而后者则是业务无关的,通过WCF本身的基础架构抛出,主要体现在对象的序列化、消息的处理、消息传输和消息的分发等等。在这里我们更多地关注与应用异常。

首先,我们在不做任何异常处理相关操作的情况下,看看如果在服务端执行某个服务操作的过程中抛出异常后,客户端会得到怎样的结果。我们通过实例的形式来演示这中场景。处于简单和易于理解考虑,我们照例沿用计算服务的例子。

我们照例采用典型的四层结构(Contract、Service、Hosting和Client),具体的层次在VS解决方案的划分如图1所示:

图1 异常抛出实例解决方案结构

下面代码片断表示服务契约(ICalculator)和服务类型(CalculatorService)的定义。为了简洁,在服务契约接口中,我们仅仅定义了唯一一个用于进行两个整数触发预算的方法Divide。服务契约和服务类型类型分别定义在项目Contracts和Services中。

   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:         int Divide(int x, int y);
   9:     }   
  10: }
   1: using Artech.WcfServices.Contracts;
   2: namespace Artech.WcfServices.Services
   3: {
   4:     public class CalculatorService : ICalculator
   5:     {
   6:         public int Divide(int x, int y)
   7:         {
   8:             return x / y;
   9:         }
  10:     }
  11: }

接下来是通过Console应用程序(Hosting项目)对上面定义的WCF服务(CalculatorService)进行寄宿(Host)的代码和相关配置。

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.WcfServices.Services;
   4: namespace Artech.WcfServices.Hosting
   5: {
   6:     public class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:            using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
  11:             {
  12:                
  13:                 host.Open();
  14:                 Console.Read();
  15:             }
  16:         }
  17:     }
  18: }
   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>  
   3:     <system.serviceModel> 
   4:         <services>
   5:             <service name="Artech.WcfServices.Services.CalculatorService">
   6:                 <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" />
   7:             </service>
   8:         </services>
   9:     </system.serviceModel>
  10: </configuration>

最后在代表客户端的Console应用程序(Client项目)中对计算服务CalculatorService进行调用。相关的服务调用代码和配置如下所示,为了让服务端在执行Divide操作的时候抛出异常,特意将第二个参数设置为0,以便服务在进行除法运算的时候抛出System.DivideByZeroException异常。

   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:                     int result = calculator.Divide(1, 0);
  17:                 }
  18:             }
  19:         }
  20: }
  21: }
   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>        
   4:        <client>         
   5:            <endpoint address="http://127.0.0.1:3721/calculatorservice"
   6:               binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" name="calculatorservice" />
   7:         </client>
   8:     </system.serviceModel>
   9: </configuration>

在启动服务寄宿程序(Hosting)后执行客户端服务调用程序,在客户端将会跑出如图2所示的类型为System.ServiceModel.FaultException的异常,其错误消息为:

“由于内部错误,服务器无法处理该请求。有关该错误的详细信息,请打开服务器上的 IncludeExceptionDetailInFaults (从 ServiceBehaviorAttribute 或从 <serviceDebug> 配置行为)以便将异常信息发送回客户端,或在打开每个 Microsoft .NET Framework 3.0 SDK 文档的跟踪的同时检查服务器跟踪日志。”

图2 客户端捕获从服务端抛出的异常

从上面的实例演示中,我们可以获知WCF在默认情况下的异常处理行为:对于服务端抛出的异常(这里主要指应用异常),客户端捕获到的总一个具有相同异常消息的System.ServiceModel.FaultException异常。由于异常类型和消息固定不变,对于服务的客户端来说,直接通过捕获到的异常相关的信息是无法确定服务端在执行服务操作的时候遇到的具体的错误是什么。

WCF如此设计的一个主要的目的为了安全。原因很简单,由于我们不能保证服务端直接抛出的异常不包含任何敏感信息,所以直接将服务端原始的异常信息暴露给客户端(对于服务提供者来说,该客户端可能使一个不受信任或者部完全受信任的第三方)。

二、 异常细节的传输

通过上面的介绍,我们已经意识到了:在默认的情况下,如果异常(主要指应用异常)在执行服务操作的过程中抛出,其真正的异常信息并不能被客户端捕获。实际上,服务端具体的异常细节信息仅限于服务端可见,并不会传递到客户端。

然后,不论对于开发阶段的调试,还是维护阶段的纠错、排错,如果在客户端调用某个服务操作后能够很直接地获取到从服务端抛出异常的所有细节,这无疑是一件很有价值的事情。那么,WCF能够做到这一点呢?答案是肯定的。

实际上,对于细心的读者,看到客户端捕获的FaultException异常的消息,就能从中找到解决方案。消息中指出,如果试图得到服务端具体的错误信息,需要开启IncludeExceptionDetailInFaults这么一个开关。具体来讲,又具有两种等效的方式:配置的方式和应用自定义特性(Custom Attribute)的方式。

通过在服务端的配置中,为寄宿的服务定义相应的服务行为(Service Behavior),并把serviceDebug配置项的includeExceptionDetailInFaults属性设为True。具体配置如下所示:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>  
   3:     <system.serviceModel> 
   4:         <behaviors>
   5:             <serviceBehaviors>
   6:                 <behavior name="serviceDebuBehavior">
   7:                     <serviceDebug includeExceptionDetailInFaults="true" />
   8:                 </behavior>
   9:             </serviceBehaviors>
  10:         </behaviors>
  11:         <services>
  12:             <service behaviorConfiguration="serviceDebuBehavior" name="Artech.WcfServices.Services.CalculatorService">
  13:                 <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsHttpBinding"                    contract="Artech.WcfServices.Contracts.ICalculator" />
  14:             </service>
  15:         </services>
  16: </system.serviceModel>
  17: </configuration>

大部分系统自定义服务行为都可以直接通过在服务类型上应用System.ServiceModel.ServiceBehaviorAttribute这么一个自定义特性一样,includeExceptionDetailInFaults服务调试(ServiceDebug)行为也不另外。在ServiceBehaviorAttribute中定义了一个IncludeExceptionDetailInFaults属性,当我们将ServiceBehaviorAttribute特性应用到具体的服务类型上的时候,只需将此属性设为true即可。

   1: [AttributeUsage(AttributeTargets.Class)]
   2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
   3: {
   4:     //其他成员    
   5: public bool IncludeExceptionDetailInFaults { get; set; }
   6: }

所以如果不采用上面的配置,在服务类型CalculatorService上面应用ServiceBehaviorAttribute特性,并进行如下的设置,也可以到达相同的效果。

   1: using Artech.WcfServices.Contracts;
   2: using System.ServiceModel;
   3: namespace Artech.WcfServices.Services
   4: {
   5:     [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
   6:     public class CalculatorService : ICalculator
   7:     {
   8:         //省略服务成员
   9: }
  10: }

当IncludeExceptionDetailInFaults被开启的ServiceDebug服务属性通过上述两种方式应用到我们例子中的服务CalculatorService的情况下,运行客户端应用程序,将会捕获包含有错误明细信息的异常,运行的结果如图3所示:

图3 客户端捕获到具有明细信息的异常

从图3中,我们可以看出客户端捕获到的实际上是一个泛型的System.ServiceModel.FaultException<TDetail>异常。FaultException<TDetail>继承自FaultException,这两种典型的异常类型在WCF异常处理中具有重要的地位,在本章后续章节中还会重点讲述,在这里先做一点简单的介绍。

对于所有从服务端抛出的异常,只有FaultException和直接或间接继承自FaultException的异常才能被序列化,并最终通过消息返回给服务的调用端。FaultException可以通过文本的形式保存相应的错误信息。FaultException<TDetail>在FaultException现有的基础上,增加了一个额外的特性:将错误信息通过一个具体的对象表示,其类型便是范型类型TDetail,该对象可以通过属性Detail设置或者获取。

   1: [Serializable]
   2: public class FaultException<TDetail> : FaultException
   3: {    
   4:      // 其他成员
   5:     public FaultException(TDetail detail);  
   6:     public TDetail Detail { get; }
   7: }

对于上面例子对应的场景,客户端捕获的异常类型实际上是FaultException< System.ServiceModel.ExceptionDetail>,也就是说其具体的泛型类型参数为System.ServiceModel.ExceptionDetail。ExceptionDetail的定义如下:

   1: [DataContract]
   2: public class ExceptionDetail
   3: {
   4:     // 其他成员
   5:     public ExceptionDetail(Exception exception);
   6:  
   7:     [DataMember]
   8:     public string HelpLink { get; private set; }
   9:     [DataMember]
  10:     public ExceptionDetail InnerException { get; private set; }
  11:     [DataMember]
  12:     public string Message { get; private set; }
  13:     [DataMember]
  14:     public string StackTrace { get; private set; }
  15:     [DataMember]
  16: public string Type { get; private set; }
  17: }

ExceptionDetail是一个数据契约(Data Contract),也就意味ExceptionDetail是一个可以被DataContractSerializer进行序列化的对象。再仔细察看具体的属性成员列表,我想很多读者肯定有一种是曾相识的感觉:是不是和System.Exception的属性成员定义很相似。实际上,ExceptionDetail是WCF专门设计出来用于封装服务端抛出的异常信息的,其个属性HelpLink、InnerException和StackTrace各自和System.Exception的同名属性向对应,而属性Type表示异常的类型。

也就是说,对于应用了开启IncludeExceptionDetailInFaults的ServiceDebug服务行为的WCF服务,在执行服务操作抛出的异常信息,可以通过包含在客户端捕获的FaultException<ExceptionDetail>异常中的ExceptionDetail对象获取。比如,在下面的代码中,我修改了客户端的代码,将具体的错误信息输出到控制台上:

   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<ExceptionDetail> ex)
  21:                     {
  22:                         Console.WriteLine("Message:{0}", ex.Detail.Message);
  23:                         Console.WriteLine("Type:{0}", ex.Detail.Type);
  24:                         Console.WriteLine("StackTrace:{0}", ex.Detail.StackTrace);
  25:                         Console.WriteLine("HelpLink:{0}", ex.Detail.HelpLink);
  26:                         (calculator as ICommunicationObject).Abort();
  27:                     }
  28:                 }
  29:             }
  30:         }
  31: }
  32: }

输出结果:

   1: Message:试图除以零。
   2: Type:System.DivideByZeroException
   3: StackTrace:   在 Artech.WcfServices.Services.CalculatorService.Divide(Int32 x, Int32 y) 位置 D:\Demos\Artech.WcfServices\Services\CalculatorService.cs:行号 13
   4:    在 SyncInvokeDivide(Object , Object[] , Object[] )
   5:    在 System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]&; outputs)
   6:    在System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&; rpc)
   7:    在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&; rpc)
   8:    在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&; rpc)
   9:    在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc&; rpc)
  10:    在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc&; rpc)
  11:    在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc&; rpc)
  12:    在 System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
  13: HelpLink:

注:在catch程序块中,我们通过代码((calculator as ICommunicationObject).Abort();)将会话信道强行中断。原因在于,对于基于会话信道(Sessionful Channel)的服务调用,服务端抛出的异常会将该信道的状态转变为出错状态(Faulted),处于Faulted状态的会话信道将不能再用于后续的通信,即使你调用Close方法将其关闭。在这种情况下,需要调用Abort方法对其进行强行中止。具体的原理,在《WCF技术剖析(卷1)》的第9章有详细的介绍。

对于服务行为SerivceDebug的IncludeExceptionDetailInFaults属性,我需要再次重申一遍:由于会导致敏感信息泄露的潜在危险,一般地我们仅仅在调试的时候才会开启该属性。对于已经发布、付诸使用的服务,这个开关一般是关闭的。实际上,我们从这个服务行为的命名也可以看出,SerivceDebug,也是用于调试服务的服务行为罢了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏王金龙的专栏

分布式系统ID生成方案汇总

本文只整理MySQL的自增字段方案,Oracle和SQL Server的自增长方案就不介绍了。

24420
来自专栏大内老A

WCF技术剖析之三十:一个很有用的WCF调用编程技巧[下篇]

在《上篇》中,我通过使用Delegate的方式解决了服务调用过程中的异常处理以及对服务代理的关闭。对于《WCF技术剖析(卷1)》的读者,应该会知道在第7章中我通...

21150
来自专栏有趣的django

面试题目及答案

1 Python的函数参数传递 看两个例子: a = 1 def fun(a): a = 2 fun(a) print a # 1 a = [] de...

1.3K90
来自专栏大史住在大前端

webpack4.0各个击破(5)—— Module篇

使用webpack对脚本进行合并是非常方便的,因为webpack实现了对各种不同模块规范的兼容处理,对前端开发者来说,理解这种实现方式比学习如何配置webpac...

14220
来自专栏开发技术

shiro源码篇 - shiro的session的查询、刷新、过期与删除,你值得拥有

    老公酷爱网络游戏,老婆无奈,只得告诫他:你玩就玩了,但是千万不可以在游戏里找老婆,不然,哼哼。。。     老公嘴角露出了微笑:放心吧亲爱的,我绝对不会...

43220
来自专栏大内老A

ASP.NET Core管道深度剖析(3):管道是如何处理HTTP请求的?

我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成,所以从总体设计来讲是非常简单的,但是就具体的实现来说,由于其中涉及很多对象的交互...

32750
来自专栏积累沉淀

Java批处理

批处理 JDBC对批处理的操作,首先简单说一下JDBC操作sql语句的简单机制。 JDBC执行数据库操作语句,首先需要将sql语句打包成为网络字...

53550
来自专栏大内老A

如何让普通变量也支持事务回滚?

有一次和人谈起关于事务的话题,谈到怎样的资源才能事务型资源。除了我们经常使用的数据库、消息队列、事务型文件系统(TxF)以及事务性注册表(TxR)等,还有那些资...

19280
来自专栏京东技术

飞哥教你使用异步编程提升服务性能

38440
来自专栏大闲人柴毛毛

轻量级线程池的实现

写在前面 最近因为项目需要,自己写了个单生产者-多消费者的消息队列模型。多线程真的不是等闲之辈能玩儿的,我花了两个小时进行设计与编码,却花了两天的时间调试与运...

55440

扫码关注云+社区

领取腾讯云代金券