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


WCF技術剖析之二十二: 深入剖析WCF底層異常處理框架實現原理[中篇]

[上篇]中,我們分別站在消息交換和編程的角度介紹了SOAP Fault和FaultException異常。在服務執行過程中,我們手工拋出FaultException異常,WCF服務端框架會對該異常對象進行序列化病最終生成Fault消息。當WCF客戶端框架介紹到該Fault消息之後,會做一項相反的操作:對Fault消息中進行解析和反序列化,重新生成並拋出FaultException異常。WCF框架自動為我們作了這麼多“幕後”工作,使得開發人員可以完全采用編寫一般的.NET應用程序的模式進行異常的處理:在錯誤的地方拋出相應異常,對於潛在出錯的方法調用進行相應的異常捕獲和處理。所以,WCF的異常處理框架的核心功能就是實現FaultException異常和Fault消息之間的轉換,接下來我們著重來討論這個話題。

一、FaultException異常和Fault消息之間的紐帶:MessageFault

對於WCF的異常處理框架,其本身並不直接進行FaultException異常和Fault消息之間的轉換,而是通過另外一個作為中介的對象來完成的,這個對象就是這一小節我們講述的重點:MessageFault。Message(Fault)、MessageFault和FaultException通過如圖1描述的“三角”關係實現了相互之間的轉化。

clip_image002

圖1 Message(Fault)、Message和FaultException“三角”轉換關係

在消息介紹MessageFault之前,我們先來看看MessageFault的定義。MessageFault定義在命名空間System.ServiceModel.Channels下,下麵的代碼是MessageFault的定義。

   1: public abstract class MessageFault
   2: {
   3:    //其他成員
   4:     public static MessageFault CreateFault(Message message, int maxBufferSize);
   5:     public static MessageFault CreateFault(FaultCode code, FaultReason reason);
   6:     public static MessageFault CreateFault(FaultCode code, string reason);
   7:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail);
   8:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail, XmlObjectSerializer serializer);
   9:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail, XmlObjectSerializer serializer, string actor);
  10:     public static MessageFault CreateFault(FaultCode code, FaultReason reason, object detail, XmlObjectSerializer serializer, string actor, string node);
  11:     
  12:     public T GetDetail<T>();
  13:     public T GetDetail<T>(XmlObjectSerializer serializer);
  14:     public XmlDictionaryReader GetReaderAtDetailContents();
  15:  
  16:     public void WriteTo(XmlDictionaryWriter writer, EnvelopeVersion version);
  17:     public void WriteTo(XmlWriter writer, EnvelopeVersion version);
  18:  
  19:     public virtual string Actor { get; }
  20:     public abstract FaultCode Code { get; }
  21:     public abstract bool HasDetail { get; }
  22:     public bool IsMustUnderstandFault { get; }
  23:     public virtual string Node { get; }
  24:     public abstract FaultReason Reason { get; }
  25: }

從上麵給出的對MessageFault並不複雜的定義可以看出,它的屬性成員和FaultException,以及SOAP Fault的5個子元素是想匹配的:Code、Reason、Node、Actor(對於SOAP 1.2規範中SOAP Fault的Role元素,在SOAP 1.1中的名稱為Actor)。而另一個元素Detail則可以通過兩個泛型方法GetDetail<T>獲得。由於此操作需要對錯誤明細對象進行反序列化,所以需要指定錯誤明細類型對應的序列化器,默認情況下采用的是DataContractSerializer。而屬性IsMustUnderstandFault表述此錯誤是否是由於識別 SOAP 標頭失敗而造成的,實際上,它和FaultCode的IsPredefinedFault向對應,主要具有預定義的Code,IsMustUnderstandFault就返回True。

通過MessageFault眾多的CreateFault靜態方法,我們可以以不同的組合方式指定構成SOAP Fault的5個元素。如果指定了錯誤明細對象,需要指定與之匹配的序列化器以實現對其的序列化和反序列化。兩個重載的WirteTo方法實行對MessageFault進行序列化,並將序列化後的XML通過XmlDictionaryWriter或者XmlWriter寫入掉相應的“流”中。

由於不同的SOAP規範的版本(SOAP 1.1和SOAP 1.2)對Message Fault的結構進行了不同的規定,所有在調用WirteTo的時候需要顯式地指定基於那個版本進行寫入(SOAP的版本通過EnvelopeVersion表示)。下麵的示例代碼中,我們創建了一個MessageFault對象,分別針對SOAP 1.1和SOAP 1.2寫到兩個不同的XML文件中。讀者可以仔細辨別最終生成的Message Fault到底有多大的差別。

   1: using System.Collections.Generic;
   2: using System.Diagnostics;
   3: using System.IO;
   4: using System.Runtime.Serialization;
   5: using System.ServiceModel;
   6: using System.ServiceModel.Channels;
   7: using System.Text;
   8: using System.Xml;
   9: namespace MessageFaultDemos
  10: {
  11:     class Program
  12:     {
  13:         static void Main(string[] args)
  14:         {
  15:             FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "https://www.artech.com/"));
  16:             IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
  17:             reasonTexts.Add(new FaultReasonText("The input parameter is invalid!","en-US"));
  18:             reasonTexts.Add(new FaultReasonText("輸入參數不合法!", "zh-CN"));
  19:             FaultReason reason = new FaultReason(reasonTexts);
  20:  
  21:             CalculationError detail = new CalculationError("Divide", "被除數y不能為零!");
  22:             MessageFault fault = MessageFault.CreateFault(code, reason, detail, new DataContractSerializer(typeof(CalculationError)), "https://https://www.artech.com/calculatorservice", "https://https://www.artech.com/calculationcenter");
  23:  
  24:             string fileName1 = @"fault.soap11.xml";
  25:             string fileName2 = @"fault.soap12.xml";
  26:             WriteFault(fault, fileName1, EnvelopeVersion.Soap11);
  27:             WriteFault(fault, fileName2, EnvelopeVersion.Soap12);           
  28:         }
  29:  
  30:         static void WriteFault(MessageFault fault, string fileName, EnvelopeVersion version)
  31:         { 
  32:              using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
  33:             {
  34:                 using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(stream, Encoding.UTF8, false))
  35:                 {
  36:                     fault.WriteTo(writer, version);
  37:                     Process.Start(fileName);
  38:                 }
  39:             }
  40:         }
  41:     }
  42: }

基於SOAP 1.1(fault.soap11.xml):

   1: <Fault xmlns="https://schemas.xmlsoap.org/soap/envelope/">
   2:   <faultcode xmlns="" xmlns:a="https://www.artech.com/">a:CalculationError</faultcode>
   3:   <faultstring xml:lang="en-US" xmlns="">The input parameter is invalid!</faultstring>
   4:   <faultactor xmlns="">https://https://www.artech.com/calculatorservice</faultactor>
   5:   <detail xmlns="">
   6:     <CalculationError xmlns="https://www.artech.com/" xmlns:i="https://www.w3.org/2001/XMLSchema-instance">
   7:       <Message>被除數y不能為零!</Message>
   8:       <Operation>Divide</Operation>
   9:     </CalculationError>
  10:   </detail>
  11: </Fault>

基於SOAP 1.2(fault.soap12.xml):

   1: <Fault xmlns="https://www.w3.org/2003/05/soap-envelope">
   2:   <Code>
   3:     <Value>Sender</Value>
   4:     <Subcode>
   5:       <Value xmlns:a="https://www.artech.com/">a:CalculationError</Value>
   6:     </Subcode>
   7:   </Code>
   8:   <Reason>
   9:     <Text xml:lang="en-US">The input parameter is invalid!</Text>
  10:     <Text xml:lang="zh-CN">輸入參數不合法!</Text>
  11:   </Reason>
  12:   <Node>https://https://www.artech.com/calculationcenter</Node>
  13:   <Role>https://https://www.artech.com/calculatorservice</Role>
  14:   <Detail>
  15:     <CalculationError xmlns="https://www.artech.com/" xmlns:i="https://www.w3.org/2001/XMLSchema-instance">
  16:       <Message>被除數y不能為零!</Message>
  17:       <Operation>Divide</Operation>
  18:     </CalculationError>
  19:   </Detail>
  20: </Fault>

二、 如何實現Message(Fault)和MessageFault之間的轉換

MessageFault可以作為Message(Fault)和FaultException異常之間進行轉換的中介,而且WCF定義個相應的API實現Message和MessageFault,以及MessageFault和FaultException異常之間的轉化。我們先來關注一下如果實現Message和MessageFault兩種之間的轉化。

由於MessageFault定義與Fault消息中主體部分的Fault元素,即SOAP Fault,所以對於一個給定的表示Fault消息的Message對象,我們可以通過提取SOAP Fault對應,從而創建相應的MessageFault對象。MessageFault提供了下麵一個CreateFault靜態方法,使我們能過傳入一個Message對象創建MessageFault(參數maxBufferSize為做大消息緩衝區最大緩衝區大小)。

   1: public abstract class MessageFault
   2: {
   3:    //其他成員
   4:     public static MessageFault CreateFault(Message message, int maxBufferSize);
   5: }

在下麵的代碼中,借助於Message的靜態方法CreateMessage,通過逐個指定FaultCode、FaultReason、Detail和Action的方式創建了一個Fault消息。然後將其傳入上述的CreateFault靜態方法,從而創建出相應的MessageFault對象。最後通過MessageFault的GetDetail<T>方法得到錯誤明細對象,通過輸出的信息可以證實該MessageFault中的錯誤明信息和創建消息指定指定的是一致的。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.ServiceModel;
   4: using System.ServiceModel.Channels;
   5: namespace MessageFaultDemos
   6: {
   7:     class Program
   8:     {
   9:         static void Main(string[] args)
  10:         {
  11:             CalculationError detail = new CalculationError("Divide","被除數y不能為零!");
  12:             FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "https://www.artech.com/"));
  13:             IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
  14:             Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, code, "被除數y不能為零!",
  15:                detail, "https://www.artech.com/calculatorservice/dividecalculationerrorfault");
  16:             MessageFault messageFault = MessageFault.CreateFault(message, int.MaxValue);
  17:             detail = messageFault.GetDetail<CalculationError>();
  18:             Console.WriteLine("Operation: {0}", detail.Operation);
  19:             Console.WriteLine("Message: {0}", detail.Message);
  20:         }
  21:     }
  22: }

輸出的結果:

Operation:Devide
Message:被除數y不能為零!

既然我們可以通過提取Fault消息的SOAP Fault進而創建相應的MessageFault,我們同樣可以通過給定的MessageFault對象,基於某種消息版本和Action報頭,創建一個Fault消息。Message類型中定義的下麵一個靜態的CreateMessage方法可以幫我們實現這樣的操作。

   1: public abstract class Message : IDisposable
   2: {    
   3:     //其他成員
   4:     public static Message CreateMessage(MessageVersion version, MessageFault fault, string action);
   5: }

下麵的例子中,我通過MessageFault的CreateFault方法創建了一個MessageFault對象。然後將其傳入上述的CreateMessage靜態方法,並指定不同的MessageVersion(MessageVersion.Soap11WSAddressingAugust2004和MessageVersion.Soap12WSAddressing10),創建了不同的Fault消息。有興趣的讀者可以仔細分析一下:基於不同的消息版本,針對同一個MessageFault對象創建的Fault消息都有哪些差異(最後能夠針對SOAP 1.1、SOAP 1.2、WS-Addressing 2004和WS-Addressing 1.0規範進行比較)。

   1: using System.Diagnostics;
   2: using System.ServiceModel;
   3: using System.ServiceModel.Channels;
   4: using System.Text;
   5: using System.Xml;
   6: namespace MessageFaultDemos
   7: {
   8:     class Program
   9:     {
  10:         static void Main(string[] args)
  11:         {
  12:             MessageFault messageFault = MessageFault.CreateFault(FaultCode.CreateSenderFaultCode("Infrastructure", "https://www.artech.com/"), "Message Timeout");
  13:             Message messageSoap11 = Message.CreateMessage(MessageVersion.Soap11WSAddressingAugust2004, messageFault, "https://www.artech.com/calculatefault");
  14:             Message messageSoap12 = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, messageFault, "https://www.artech.com/calculatefault");
  15:             using (XmlWriter writer1 = new XmlTextWriter("faultmessage.soap11.addressing2004.xml", Encoding.UTF8))
  16:             using (XmlWriter writer2 = new XmlTextWriter("faultmessage.soap12.addressing10.xml", Encoding.UTF8))
  17:             {
  18:                 messageSoap11.WriteMessage(writer1);
  19:                 messageSoap12.WriteMessage(writer2);
  20:             }
  21:             Process.Start("faultmessage.soap11.addressing2004.xml");
  22:             Process.Start("faultmessage.soap12.addressing10.xml");            
  23:         }
  24:        
  25:     }
  26: }

基於SOAP 1.1 + WS-Addressing 2004的Fault消息(faultmessage.soap11.addressing2004.xml):

   1: <s:Envelope xmlns:a="https://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:s="https://schemas.xmlsoap.org/soap/envelope/">
   2:   <s:Header>
   3:     <a:Action s:mustUnderstand="1">https://www.artech.com/calculatefault</a:Action>
   4:   </s:Header>
   5:   <s:Body>
   6:     <s:Fault>
   7:       <faultcode xmlns:a="https://www.artech.com/">a:Infrastructure</faultcode>
   8:       <faultstring xml:lang="en-US">Message Timeout</faultstring>
   9:     </s:Fault>
  10:   </s:Body>
  11: </s:Envelope>

基於SOAP 1.2 + WS-Addressing 1.0的Fault消息(faultmessage.soap12.addressing10.xml):

   1: <s:Envelope xmlns:a="https://www.w3.org/2005/08/addressing" xmlns:s="https://www.w3.org/2003/05/soap-envelope">
   2:   <s:Header>
   3:     <a:Action s:mustUnderstand="1">https://www.artech.com/calculatefault</a:Action>
   4:   </s:Header>
   5:   <s:Body>
   6:     <s:Fault>
   7:       <s:Code>
   8:         <s:Value>s:Sender</s:Value>
   9:         <s:Subcode>
  10:           <s:Value xmlns:a="https://www.artech.com/">a:Infrastructure</s:Value>
  11:         </s:Subcode>
  12:       </s:Code>
  13:       <s:Reason>
  14:         <s:Text xml:lang="en-US">Message Timeout</s:Text>
  15:       </s:Reason>
  16:     </s:Fault>
  17:   </s:Body>
  18: </s:Envelope>

三、如何實現MessageFault和FaultException之間的轉換

上麵介紹的是MessageFault和Message(Fault)之間的轉化關係,現在我們來介紹Message、Message和FaultException“三角”關係中的另一組轉換關係:MessageFault和FaultException之間的轉換關係。

WCF將實現MessageFault和FaultException之間的轉化的API定義在FaultException類中。其中兩個靜態CreateFault方法實現將MessageFault向FaultException的轉換,而實例方法CreateMessageFault則將FaultException對象轉化成相應的MessageFault對象。三個方法定義如下,其中faultDetailTypes代表錯誤明細類型列表,這是為對FaultException<TDetail>對象的反序列化服務的。

   1: [Serializable]
   2: public class FaultException : CommunicationException
   3: {
   4:     //其他成員
   5:     public static FaultException CreateFault(MessageFault messageFault, params Type[] faultDetailTypes);
   6:     public static FaultException CreateFault(MessageFault messageFault, string action, params Type[] faultDetailTypes);
   7:  
   8:     public virtual MessageFault CreateMessageFault();
   9: }

在下麵的實例代碼中,先通過調用MessageFault的靜態方法CreateFault方法,傳入組成一個完整MessageFault相關的參數,創建了一個MesageFault對象。然後調用上麵介紹的靜態方法CreateFault,創建FaultException對象。由於我們構建MessageFault的時候查傳入一個CalculationError作為錯誤明細,所以返回的異常類型應該是FaultException<CalculationError>對象。最後,我們將該異常對象的相關信息在控製台上輸出。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Runtime.Serialization;
   4: using System.ServiceModel;
   5: using System.ServiceModel.Channels;
   6: namespace MessageFaultDemos
   7: {
   8:     class Program
   9:     {
  10:         static void Main(string[] args)
  11:         {
  12:             FaultCode code = FaultCode.CreateSenderFaultCode(new FaultCode("CalculationError", "https://www.artech.com/"));
  13:             IList<FaultReasonText> reasonTexts = new List<FaultReasonText>();
  14:             reasonTexts.Add(new FaultReasonText("The input parameter is invalid!", "en-US"));
  15:             reasonTexts.Add(new FaultReasonText("輸入參數不合法!", "zh-CN"));
  16:             FaultReason reason = new FaultReason(reasonTexts);
  17:             CalculationError detail = new CalculationError("Divide", "被除數y不能為零!");
  18:             MessageFault fault = MessageFault.CreateFault(code, reason, detail, new DataContractSerializer(typeof(CalculationError)), "https://https://www.artech.com/calculatorservice", "https://https://www.artech.com/calculationcenter");
  19:  
  20:             FaultException<CalculationError> exception = FaultException.CreateFault(fault, typeof(CalculationError)) as FaultException<CalculationError>;
  21:             Console.WriteLine("Fault Code: {0}",exception.Code.Name);
  22:             Console.WriteLine("\tSubCode: {0}:{1}", exception.Code.SubCode.Namespace,exception.Code.SubCode.Name);
  23:             Console.WriteLine("Fault Reason:");
  24:             foreach (var reasonText in exception.Reason.Translations)
  25:             {
  26:                 Console.WriteLine("\t{0}:{1}", reasonText.XmlLang, reasonText.Text);
  27:             }
  28:             Console.WriteLine("Detail:");
  29:             Console.WriteLine("\tOperation:{0}", exception.Detail.Operation);
  30:             Console.WriteLine("\tMessage:{0}", exception.Detail.Message);
  31:         }
  32:     }
  33: }

輸出的結果:

Fault Code: Sender
        SubCode: http://www.artech.com/:CalculationError
Fault Reason:
        en-US:The input parameter is invalid!
        zh-CN:輸入參數不合法!
Detail:
        Operation:Divide
        Message:被除數y不能為零!

上麵給出的是如果將一個MessageFault對象轉換成一個FaultException異常的例子,如果要進行相幹的操作,隻需要直接調用FaultException異常實例的CreateMessageFault方法即可。清楚了應該調用怎樣的API進行MessageFault和FaultException之間的轉換,我們現在來進一步深入了解其內部的實現原理。在自身的異常處理框架內容,WCF實際上是通過一個特殊的對象實現兩者之間的轉換的,這個對象就是我們下麵要介紹的FaultFormatter。

四、FaultException與MessageFault轉換的核心:FaultFormatter

在《WCF技術剖析(卷1)》的第5章關於序列化和數據契約的介紹中,我們談到:WCF借助於一個特殊的對象——MessageFormatter,實現方法調用和消息之間的轉換。具體來說,客戶端通過ClientMessageFormatter將服務操作方法調用轉換成請求消息(其中主要涉及對參數對象的序列化),以及將接收到的回複消息轉換成服務操作方法對應的返回值或者輸出/引用參數(其中隻要涉及對返回值或者輸出/引用參數的反序列化);服務端則通過DispatchMessageFormatter實現與此相反的操作。

MessageFormatter實現了在正常的服務調用過程中方法調用和消息之間的轉換,但是,當異常(這裏指的是FaultException異常)從服務端拋出,WCF通過需要一個相似的組件實現類似的功能:在服務端對異常對象進行序列化並生成回複消息(Fault消息),在客戶端對接收到的回複消息進行反序列化重建並拋出異常。這樣的一個使命由FaultFormatter擔當,不過,由於MessageFault是FaultException和Fault消息進行轉換的中介,所以FaultFormatter並不直接進行兩者之間的轉換,而是實現FaultException和MessageFault之間的轉換。

嚴格地說來,FaultFormatter僅僅是WCF一個內部對象,但是對該對象的深刻認識將非常有助於我們有效的理解WCF整個異常處理機製。FaultFormatter在客戶端和服務端所扮演的角色是不同的:客戶端將通過解析回複Fault消息生成的MessageFault轉換成FaultException異常,以便後續的步驟建起拋出;服務端在將拋出的FaultException異常轉換成MessageFault,以便後續的步驟生成相應的Fault消息。客戶端和服務端這種職責的不同可以通過下麵兩個接口的定義看出來:

internal interface IClientFaultFormatter
{
    FaultException Deserialize(MessageFault messageFault, string action);
}
internal interface IDispatchFaultFormatter
{
    MessageFault Serialize(FaultException faultException, out string action);
}

內部(Internal)接口IClientFaultFormatter和IDispatchFaultFormatter分別定義了FaultFormatter在客戶端和服務端的職能,即它們分別實現對FaultException對象的反序列化和序列化。在對FaultException對象進行序列化需要提取Action屬性作為Fault消息的Action報頭;而將MessageFault進行反序列化生成FaultException對象的時候需要從外部指定Action屬性的值,所以兩個方法各有一個action參數。

WCF定義了一個內部類System.ServiceModel.Dispatcher.FaultFormatter實現了這兩個接口,並將其作為服務端和客戶端的FaultFormatter。下麵是FaultFormatter類型的定義:

internal class FaultFormatter : IClientFaultFormatter, IDispatchFaultFormatter
{
    public FaultException Deserialize(MessageFault messageFault, string action);
    public MessageFault Serialize(FaultException faultException, out string action);
}

由於WCF將絕大部分序列化和反序列化的工作都交付給兩個序列化器:DataContractSerializer和XmlSerializerObjectSerializer,對於FaultException異常對象的序列化自然也不例外。為此,WCF定義了兩個具體的類型System.ServiceModel.Dispatcher.DataContractSerializerFaultFormatter和System.ServiceModel.Dispatcher.XmlSerializerFaultFormatter。它們直接繼承自FaultFormatter,分別采用DataContractSerializer和XmlSerializerObjectSerializer作為相應的序列化器。IClientFaultFormatter、IDispatchFaultFormatter、FaultFormatter、DataContractSerializerFaultFormatter和XmlSerializerFaultFormatter之間的關係可以簡單地通過圖2所示的類圖表示。

clip_image004

圖2 FaultFormatter體係結構


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

最後更新:2017-10-30 11:34:29

  上一篇:go  MySQL+PHP大文件讀取和寫庫
  下一篇:go  WCF技術剖析之二十三:服務實例(Service Instance)生命周期如何控製[上篇]