【读书笔记】
在进行分布式应用的异常处理时需要解决和考虑的基本要素:
- 异常的封装:服务端抛出的异常如何序列化传递到客户端
- 敏感信息的屏蔽:抛出的异常往往包含一些敏感的信息,直接将服务操作执行过程抛出的异常信息抛出到客户端,存在极大风险
- 系统的集成和互操作:基于不同厂商和技术平台系统之间的有效集成和互操作也给异常处理提出了新的要求,基于平台A的服务队异常的描述弱项被基于平台B的客户端理解,需要按照一种厂商中立的标准来规范对异常的描述
1. 异常处理模式
异常从服务端抛出
- 异常类型:应用异常(Application Exception)和基础结构异常(Infrastructure Exception). 前者与业务相关; 后者业务无关,主要体现在:对象序列化,消息的处理,消息传输和消息分发等.
异常细节的传播
- 将IncludeExceptionDetailInFaults设置为true,可将异常细节暴露出来.
- 客户端捕获的实际上是一个泛型的System.ServiceModel.FaultException异常. FaultException<
TDetail>继承自FaultException - 所有从服务端抛出的异常,只有类型为FaultException(或其子类)的异常才能直接被序列化并最终通过消息返回给客户端.FaultException允许将错误细节定义在一个对象上,而泛型参数TDetail表示这个对象的类型.
[Serializable] public class FaultException<TDetail>:FaultException { //Else Member public FaultException(TDetail detail); public TDetail Detail {get;} //只读属性,表示指定的错误细节对象 }
- 不特殊指定,客户端捕获的异常类型实际上是FaultException. 也就是说,其具体的泛型参数为System.ServiceModel.ExceptionDetail.
[DataContract] public class ExceptionDetail { //Else Member public ExceptionDetail(Exception ex); [DataMember] public string HelpLink {get; private set;} [DataMember] public ExceptionDetail InnerException{get; private set;} [DataMember] public string Message {get;private set;} [DataMember] public string StackTrace {get; private set;} [DataMember] public string Type {get; private set;} }
实际上,ExceptionDetail是WCF专门设计出来用于封装服务端抛出的异常信息的,属性HelpLinek,InnerException,StackTrack对应Exception的同名属性,属性Type表示异常的类型.
- 两种极端的异常传播机制:要么完全对客户端屏蔽,要么完全暴露于客户端
自定义异常信息
- 通过FaultException直接指定错误信息
- 通过FaultException采用自定义类型封装错误(可序列化对象,有SerializableAttribute或者DataContract)
[DataContract(Namespace="http://www.xiaoxiao.com")] public class CalculationError { public CalculationError(string operation,string message) { this.Operation=operation; this.Message = message; } [DataMember] public string Operation {get; set;} [DataMembar] public string Message {get; set;} } //Usage var error=new CalcuationError("Divide","被除数y不能为0"); throw new FaultException<CalcuationError>(error,error.Message);
但是客户端并不会捕获异常类型FaultException, 而是未被处理的FaultException.
错误契约(Fault Contract)
客户端能够捕获到服务端抛出的FaultException异常,并不是说客户端捕获的异常就是服务端抛出的异常. 这个过程经历了对异常的序列化和反序列化过程. 错误细节对象呗学历恶化为XML并通过SOAP消息的形式返回到客户端,客户端需要通过反序列化以重建错误细节对象.而反序列化的前提是需要知道其类型. 错误契约(FaultContract)帮助将作为错误细节对象的类型确定下来.
- WCF通过System.ServiceModel.FaultContractAttribute特性定义错误契约. 由于错误契约是基于操作级别的,所以该特性应用于服务契约类型的操作契约方法成员上.
[AttributeUsage(AttributeTargets.Methord,AllowMultiple=true, Inherited=false)] public sealed class FaultContractAttribute:Attribute { public FaultContractAttribute(Type detailType); public string Action {get; set;} //错误消息<Action>报头的值.如果没有被显示指定,那么它将按照先的规则进行指定:{服务契命名空间}/{服务契约名称}/{操作契约名称}{细节类型名称}Fault; public Type DetailType {get;} //用于封装错误信息的错误细节类型 public string Name {get; set;} //错误细节被学历序列成XML元素的名称, 未显示指定,将使用DetailType对应的数据契约名称 public string Namespace {get;set;} //命名空间 未显示指定,将使用DetailType对应的数据契约命名空间 public bool HasProtectionLevel {get;} //保护级别 public ProtectionLevel ProtectionLevel {get; set;} //保护级别 } [ServiceContract(Namespace="http://www.xiaoxiao.com")] public interface ICalculator { [OperationContract] [FaultContract(typeof(CalculatorError))] int Divide(int x, int y); }
- 错误契约是服务元数据(Metadata)的一部分. 当服务元数据通过WSDL的形式被发布后,错误契约作为操作描述的一部分被写入.
- 对应错误契约的应用,不仅仅是在将自定义的错误细节类型(比如我们定义的CalculatorError)应用到服务契约响应操作上时才需要显示的在操作方法上应用FaultContractAttribute特性,对于一些极端类型(如:Int32,string等),也需要这么做.
//usage throw new FaultException<string>("y can‘t to be zero!"," the parameter y can‘t to be zero!"); [OperationContract] [FaultContract(typeof(string))] int Divide(int x, int y);
通过XmlSerializer对错误细节对象序列化
- WCF提供了DataContractSerializer和XmlSerializer. 默认序列化器是DataContractSerializer
- 可以通过使用XmlSerializerFormatAttribute特性选择XmlSerializer完成序列化和反序列化工作. 默认情况下,XmlSerializerFormatAttribute特性仅仅控制操作的参数和返回值的福利序列化行为,而不能控制错误细节对象的序列化行为. 可以通过SupportFaults=true来显示地选择XmlSerializer作为错误细节对象的序列化器
[OperationContract] [FaultContract(typeof(CalcuatorError),Name="CalculationError")] [XmlSerializerFormat(SupportFaults=true)] int Divide(int x, int y);
错误消息与FaultException异常
- 编程角度: 错误信息的载体是FaultException; 消息交换角度:错误信息则是通过错误消息来承载的.
FautlNode
由于在整个SOAP消息的路由过程中,错误可能发生在最终接收节点,也可能发生在中间节点,因此为了使SOAP错误消息的接收者能够判断导致错误的SOAP节点类型,在生成错误消息的时候,可以通过Node元素指定节点的类型.
唯一可被传播的异常: FaultException
WCF体系下,数据存在的形态大体分为两种:XML和托管对象.
FaultException异常和错误消息之间的转换
- Message <=> MessageFault <=> FaultException
//Message 转 MessageFault: public class MessageFault { public static MessageFault CreateFault(Message message, int maxBufferSize) } //MessageFault 转 Message : public class Message { public static Message CreateMessage(MessageVersion version, MessageFault fault, string action) }
WCF中将实现MessageFault和FaultException之间的转换的应用编程接口在FaultException中,两个静态方法实现MessageFault向FaultException的转换,而实例方法CreateMessageFault则将FaultException转换为MessageFault对象.其中faultDetailTypes表示错误细节类型列表,这是为对FaultException对象的反序列化服务的.
public class FaultException:CommunicationException { //Else public static FaultException CreateFault(MessageFault messageFault,params Type[] faultDetailTypes); public static FaultException CreateFault(MessageFault messageFault, string action, params Type[] faultDetailTypes); public virtual MessageFault CreateMessageFault(); }
FaultException 和 MessageFault转换的核心 FaultFormatter
MessageFormater实现了在正常的服务调用过程中方法调用和消息之间的转换. 但是当异常(这里只FaultException)从服务端抛出后,WCF需要一个类似实现类似功能, 即在服务端对异常对象进行序列化并生成错误消息,在客户端对接收到的错误消息进行反序列化重建并抛出异常. 该使命由FaultFormatter担当
FaultFormatter在客户端和服务端所扮演的角色是不同的:
- 客户端通过解析回复错误消息生成的MessageFault,该MessageFault最终被转换成FaultException异常并抛出
- 服务端则将抛出的FaultException异常转换成MessageFault,以便后学的步骤生成响应的错误消息
internal interface IClientFaultFormatter { FaultException Deserialize(MessageFault messageFault, string action); } internal interface IDispatchFaultFormatter { MessageFault Serialize(FaultException faultException, out string action); } internal class FaultFormatter:IClientFaultFormatter,IDispatchFaultFormatter { public FaultException Deserialize(MessageFault messageFault, string action); public MessageFault Serialize(FaultException faultException, out string action); }
WCF异常处理体系剖析
- 消息交换是WCF进行通信的唯一手段,消息不仅仅是正常服务调用请求和服务的载体,服务端抛出的异常也是通过消息的形式传向客户端的.
- FaultFormatter在服务端端创建于服务端寄宿之时. 在ServiceHost被初始化的过程中,WCF会为服务的每个终结点创建响应的终结点分发器(EndpointDispatcher). 而对弈每一个被创建出来的终结点分发器都具有一个响应的分发运行时(DispatchRuntime). 分发运行时是整个WCF运行时框架的核心,一系列的对象和组件被它引用以实现对整个消息分发和操作执行行为的控制.
异常的抛出,序列化,反序列化和捕获
如果服务操作在执行过程中抛出FaultException异常,WCF会获取当前分发操作的FaultFormatter,调用Serialize方法对异常对象进行序列化. 序列化完成后得到相应的MessageFault对象和Action值,这两个值最终通过调用Message的CreateMessage静态方法生成一个错误消息对象
客户端的服务调用最终通过客户端操作对象完成.当调用服务获得回复消息后,如果回复消息是错误消息,WCF会调用MessageFault的CreateFault将消息转换成MessageFault对象,并获取Action值. 最终通过客户端操作的得到FaultFormatter,传入MessageFault对象和Action值调用Deserialize方法在客户端重建FaultException异常对象并将其抛出来
ExceptionDetial为何能被反序列化
对于应用了IncludeExceptionDetailInFaults属性的True的ServiceDebugBehavior服务行为,客户端是如何将错误消息中显示错误细节的XML反序列化成ExceptionDetail? 由于我们不曾通过FaultContractAttribute特性将ExceptionDetail类型应用在相应的操作方法中,因此FaultFormatter无法确定反序列化的对象类型。
原因是WCF在初始化FaultFormatter的时候会给予ExceptionDetail类型创建FaultContractInfo对象,并将其添加到属于自己的FaultContract列表中。
WCF异常处理扩展
错误处理器实现了System.ServiceModel.Dispatcher.IErrorHandler
public interface IErrorHandler
{
bool HandlerException(Exception error);
}
void ProvideFault(Exception error, MessageVersion version, ref Message fault);
- WCF运行时角度,错误处理器隶属于信道分发器,它维护着错误处理器列表。可以将多个错误处理器应用到相应的信道分发器上,这些分发器最终连成一个管道。每个错误处理器实现某个单一异常处理任务,如日志记录,敏感信息屏蔽等
public class ChannelDispatcher:ChannelDispatcherBase { //else public Collection<IErrorHandler> ErrorHandlers {get;} }
- 异常抛出后,WCF会遍历响应信道分发器的ErrorHandlers属性表示的错误处理集合,并调用错误处理器的ProvideFault方法.
对于Provider方法的执行,有一些值得注意的地方。该方法的执行发生在绘画终结之前,并且是在执行操作方法的线程中执行的。换句话说,ProviderFault方法是以与操作方法同步的方式执行的,所以在自定义ErrorHandler的时候,不应该将一些比较耗时的操作实现放在ProviderFault中。
- 当与信道分发器关联的所有错误处理器的ProvideFault执行后,错误消息被传送给客户端.此后,错误处理器的HandleError方法被依次调用(异步方式执行),用于执行一些本地的异常处理操作(服务端本地行为,与客户端无关). 此时,可以实现一些相对耗时的操作.如 异常日志记录