閱讀517 返回首頁    go 阿裏雲 go 技術社區[雲棲]


WCF技術剖析之二十四: ServiceDebugBehavior服務行為是如何實現異常的傳播的?

服務端隻有拋出FaultException異常才能被正常地序列化成Fault消息,並實現向客戶端傳播。對於一般的異常(比如執行Divide操作拋出的DivideByZeroException),在默認的情況下,異常信息無法實現向客戶端傳遞。但是,倘若為某個服務應用了ServiceDebugBehavior這麼一個服務行為,並開啟了IncludeExceptionDetailInFaults開關,異常信息將會原封不動地傳播到客戶端。WCF內部是如何處理拋出的非FaultException異常的呢?

實際上,WCF對非FaultException異常的處理並不複雜,我們現在就來簡單介紹一下相關的流程:在執行服務操作過程中,如果拋出一個非FaultException異常,WCF會先判斷IncludeExceptionDetailInFaults開發是否開啟,如果沒有,WCF會手工創建一個MessageFault對象,並根據當前線程的語言文化從資源文件中獲取一段固定的文本作為MessageFaultFaultReason(就是我們在《WCF基本的異常處理模式[上篇]》的例子中看到的那段文字)。此外,固定的FaultCode被創建出來作為該MessageFault的Code。最終,WCF將該MessageFault轉換成一個Fault消息,並采用固定的Action作為該消息的Action報頭。所以,無論服務端拋出怎樣的異常,客戶端捕獲的總是具有相同信息的FaultException異常。

注:客戶端的錯誤信息總是這麼一段文字:“由於內部錯誤,服務器無法處理該請求。有關該錯誤的詳細信息,請打開服務器上的 IncludeExceptionDetailInFaults (從 ServiceBehaviorAttribute 或從 <serviceDebug> 配置行為)以便將異常信息發送回客戶端,或在打開每個 Microsoft .NET Framework 3.0 SDK 文檔的跟蹤的同時檢查服務器跟蹤日誌。”

上麵說的是IncludeExceptionDetailInFaults開關關閉的情況。如果IncludeExceptionDetailInFaults開啟,WCF則會基於該異常對象創建ExceptionDetail對象,並將該對象作為明細對象創建MessageFault(采用固定FaultCode)。最終,將此MessageFault轉換生成Fault消息,當然Action也是采用固定的預定值。因此,在這種情況下,服務端拋出的信息總是能夠原封不動地傳遞到客戶端。而客戶端捕獲的總是一個泛型的FaultException<ExceptionDetail>異常。

一、ExceptionDetail對象為何能被反序列化?

對於異常對象的序列化和反序列化工作,最終都回落在FaultFormatter這麼一個對象上(具體原理,可以參考《深入剖析WCF底層異常處理框架實現原理[中篇]》)。無論是虛列化還是反序列化,都具有一個根本的前提:確定對象的類型。FaultFormatter依賴創建時指定的一個FaultContractInfo列表來獲知具體的類型,而該列表最初來源於應用在操作方法上FaultContractAttribute特性的定義。

那麼,對於應用了ServiceDebugBehavior服務行為,並開啟了IncludeExceptionDetailInFaults的場景,客戶端如何能夠把承載與Fault消息中的表示錯誤明細的XML片段通過反序列化生成ExceptionDetail對象的呢?由於我們不曾通過FaultContractAttribute特性將ExceptionDetail類型應用在相應的操作方法上麵,FaultFormatter無法確定反序列化對象的類型,照理說反序列化是無法成功的,這是為何呢?

其實原因很簡單,WCF在初始化FaultFormatter的時候會基於ExceptionDetail類型創建FaultContractInfo對象,並將其添加到屬於自己的FaultContractInfo列表中。相應的實現基本上可以通過下麵的偽代碼體現:

   1: internal class FaultFormatter : IClientFaultFormatter, IDispatchFaultFormatter
   2: {
   3:     //其他成員
   4:     private FaultContractInfo[] faultContractInfos;
   5:     internal FaultFormatter(SynchronizedCollection<FaultContractInfo> faultContractInfoCollection)
   6:     {
   7:         List<FaultContractInfo> list= new List<FaultContractInfo>(faultContractInfoCollection);
   8:         faultContractInfoCollection.Add(new FaultContractInfo("https://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher/fault", typeof(ExceptionDetail)));
   9:         this.faultContractInfos = list.ToArray();
  10:     }    
  11: }

二、如果直接拋出FaultException<ExceptionDetail>異常呢?

既然FaultFormatter能夠自動實現基於ExceptionDetail對象的序列化和反序列化,那麼就意味著我們可以在具體的服務操作中直接拋出FaultException<ExceptionDetail>異常,而無須再將ExceptionDetail作為錯誤契約類型通過FaultContractAttribute特性應用到相應的服務操作上麵了。同樣以我們的計算服務為例,在Divide方法中我們直接用ExceptionDetail封裝在運算過程中拋出的異常,最終拋出FaultException<ExceptionDetail>異常。

   1: using System.ServiceModel;
   2: using Artech.WcfServices.Contracts;
   3: using System;
   4: namespace Artech.WcfServices.Services
   5: {
   6:     [ServiceBehavior(Namespace="https://www.artech.com/")]
   7:     public class CalculatorService : ICalculator
   8:     {
   9:         public int Divide(int x, int y)
  10:         {            
  11:             try
  12:             {
  13:                 return x / y;
  14:             }
  15:             catch (Exception ex)
  16:             {
  17:                 throw new FaultException<ExceptionDetail>(new ExceptionDetail(ex), ex.Message);
  18:             }
  19:         }
  20:     }
  21: }

在客戶端,我們就可以直接捕獲FaultException<ExceptionDetail>異常了。下麵的代碼中,我們將捕獲的FaultException<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("Action: {0}", ex.Action);
  23:                         Console.WriteLine("Code: {0}:{1}", ex.Code.Namespace,ex.Code.Name);
  24:                         Console.WriteLine("Detail");
  25:                         Console.WriteLine("\tMessage: {0}", ex.Detail.Message);
  26:                         Console.WriteLine("\tType: {0}", ex.Detail.Type);
  27:                     }
  28:                 }
  29:             }
  30:             Console.Read();
  31:         }
  32: }
  33: }

輸出結果(注意Action正好和在FaultFormatter中指定的值是一致的):

   1: Action: http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher/fault
   2: Code:http://www.w3.org/2003/05/soap-envelope:Sender
   3: Detail:
   4:     Message:試圖除以零。
   5:     Type:System.DivideByZeroException

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:04:41

  上一篇:go  WCF技術剖析之二十二: 深入剖析WCF底層異常處理框架實現原理[下篇]
  下一篇:go  【雲計算的1024種玩法】手把手教你如何編譯一個高性能 OpenResty