企业如何通过vue小程序开发满足高效运营与合规性需求
591
2022-11-18
Enterprise Library深入解析与灵活应用(8):WCF与Exception Handling AppBlock集成[上]
一、 基本原理介绍
在一个基于WCF的分布式应用中,服务端和客户端需要进行单独的异常处理。在服务端,让EHAB处理抛出的异常是很容易的,我们只需要按照上面代码所示的方式调用ExcpetionPolicy的HandleException方法,传入抛出的异常并指定相应的异常处理策略名称即可。关键的是如何实现让EHAB处理客户端进行服务调用抛出的异常。
我们知道,客户端进行 服务调用抛出的异常类型总是FaultException(包括FaultException
理论上来讲,我们需要的正是这种方式的异常处理方式。但是在快速开发中,这样的方式不太具有可操作性,因为异常的一个本质属性就是具有不可预测性。对于某项服务操作,不太可能罗列出所有的错误场景并抛出相应类型的异常。这也正是我们需要一种像EHAB这样一种可配置的异常处理框架的原因,这样我们才能够通过修改相应的配置为某个我们之前没有预知的异常定义相应的异常处理策略。
我们接下来的介绍的解决方案通过一种变通的方式解决了上面的问题,这种方式与通过ServiceDebugBehavior实现异常的传播有点类似:服务端抛出的异常先通过EHAB按照配置好的异常处理策略进行相应的处理。然后将处理后的异常相关的信息(包括异常类型的AssemblyQualifiedName)封装到一个类似于ExceptionDetail的可序列化对象中。最后,以该对象为基础创建MessageFault,并进一步生成Fault消息传回客户端;客户端在接收到该Fault消息后,提取服务端异常相关的信息利用反射重建异常对象(已经明确了异常类型的AssemblyQualifiedName使异常对象的重建变成可能)比将其抛出。那么对于客户端的应用程序来说,就像是捕获从服务端抛出的异常一样了。通过EHAB针对客户端配置的异常处理策略对抛出的异常进行处理,那么这种异常处理方式依然是场景驱动的。
在本例中,我们通过如下一个名称为ServiceExceptionDetail的类型来封装异常相关信息。为了简单起见,我直接让ServiceExceptionDetail继承自ExceptionDetail。由于ServiceExceptionDetail对象需要从服务端向客户端传递,我将其定义成数据契约。在ServiceExceptionDetail仅仅定义了一个唯一的属性成员:AssemblyQualifiedName,表示异常的类型的程序集有效名称,这是为了基于反射的异常重建的需要。在ServiceExceptionDetail中,定义了3个字符串常量表示对应SOAP Fault的SubCode名称和命名空间,以及对应Fault消息的Action。整个解决方法实现的原理大体上可以通过图1示。
图1 WCF与EHAB集成实现原理
注:有人会觉得这和开启了IncludeExceptionDetailInFaults开关的ServiceDebugBehavior服务行为一样,异常信息会完全暴露给客户端,会存在敏感信息泄露的危险。实际上,如果你将敏感信息屏蔽的操作定义在相关的异常处理策略中,并通过EHAB来实现,那么最终传递给客户端的信息已经是经过处理的了。
二、异常处理、封装与重建
从上面给出的整个解决方案实现原理介绍中,我们可以看出,这个结构体系需要解决如下三个功能:
通过EHAB处理服务端抛出的原始异常(XxxException):利用EHAB针对预定义的异常处理策略对服务操作抛出的异常进行处理; 通过MessageFault封装EHAB处理后的异常(YyyException):创建ServiceExceptionDetail对象封装通过EHAB处理后的异常,进而创建MessageFault对象,最终创建Fault消息将异常相关的信息向客户端传递; 客户端实现异常的重建(YyyException):客户端接收到Fault消息后,提取异常相关信息,重建异常对象,使得客户端可以利用EHAB针对基于客户端的异常处理策略对其进行相应的处理。
在本例中,我们通过两个重要的WCF组件实现对以上3个功能的实现,其中前两个通过自定义的ErrorHandler实现,最后一个通过MessageInspector实现。我们现在就来介绍这两个组件的实现方式。
1、自定义ErrorHandler实现基于EHAB的异常处理和封装
为了实现利用EHAB自动处理服务操作抛出的异常,已经对处理后异常的封装和传递,我定义了如下一个自定义的ErrorHandler:ServiceErrorHandler。
1: using System; 2: using System.ServiceModel; 3: using System.ServiceModel.Channels; 4: using System.ServiceModel.Dispatcher; 5: using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling; 6: namespace Artech.EnterLibIntegration.WcfExtensions 7: { 8: public class ServiceErrorHandler : IErrorHandler 9: { 10: public string ExceptionPolicyName 11: { get; private set; } 12: public ServiceErrorHandler(string exceptionPolicyName) 13: { 14: if (string.IsNullOrEmpty(exceptionPolicyName)) 15: { 16: throw new ArgumentNullException("exceptionPolicyName"); 17: } 18: this.ExceptionPolicyName = exceptionPolicyName; 19: } 20: 21: #region IErrorHandler Members 22: public bool HandleError(Exception error) 23: { 24: return false; 25: } 26: public void ProvideFault(Exception error, MessageVersion version, ref Message fault) 27: { 28: if(typeof(FaultException).IsInstanceOfType(error)) 29: { 30: return; 31: } 32: 33: try 34: { 35: if (ExceptionPolicy.HandleException(error, this.ExceptionPolicyName)) 36: { 37: fault = Message.CreateMessage(version, BuildFault(error), ServiceExceptionDetail.FaultAction); 38: } 39: } 40: catch (Exception ex) 41: { 42: fault = Message.CreateMessage(version, BuildFault(ex), ServiceExceptionDetail.FaultAction); 43: } 44: } 45: 46: private MessageFault BuildFault(Exception error) 47: { 48: ServiceExceptionDetail exceptionDetail = new ServiceExceptionDetail(error); 49: return MessageFault.CreateFault(FaultCode.CreateReceiverFaultCode(ServiceExceptionDetail.FaultSubCodeName, ServiceExceptionDetail.FaultSubCodeNamespace), 50: new FaultReason(error.Message), exceptionDetail); 51: } 52: 53: #endregion 54: } 55: }
在ServiceErrorHandler中定义的只读属性ExceptionPolicyName表示服务端配置的异常处理策略的名称,该属性在构造函数中指定。在ProvideFault方法中,先判断抛出的异常是否是FaultException,如果是则不作处理(在这种情况下,一般是服务提供者人为抛出的,并不希望再作进一步的处理)。否则调用ExceptionPolicy的HandleException方法,传入异常处理策略名称,对该异常进行处理。对于处理后的异常,通过BuildFault方法创建ServiceExceptionDetail对象对异常信息进行封装,并最终生成Fault消息。
2、自定义MessageInspector实现异常的重建
当封装有异常信息的Fault消息返回到客户端后,需要将异常信息提取出来并通过反射重建并抛出异常对象,我们通过自定义MessageInspector来实现这样的功能。为此,我们定义了如下一个实现了IClientMessageInspector接口的类型:ExceptionHandlingMessageInspector。
1: using System.ServiceModel; 2: using System.ServiceModel.Channels; 3: using System.ServiceModel.Dispatcher; 4: using System; 5: namespace Artech.EnterLibIntegration.WcfExtensions 6: { 7: public class ExceptionHandlingMessageInspector : IClientMessageInspector 8: { 9: public void AfterReceiveReply(ref Message reply, object correlationState) 10: { 11: if (!reply.IsFault) 12: { 13: return; 14: } 15: 16: if (reply.Headers.Action == ServiceExceptionDetail.FaultAction) 17: { 18: MessageFault fault = MessageFault.CreateFault(reply, int.MaxValue); 19: if(fault.Code.SubCode.Name == ServiceExceptionDetail.FaultSubCodeName && 20: fault.Code.SubCode.Namespace == ServiceExceptionDetail.FaultSubCodeNamespace) 21: { 22: FaultException
在AfterReceiveReply方法中,通过比较Fault消息的Action,以及SubCode的名称和命名空间确定接收到的消息正是服务端通过我们自定义的ServiceErrorHandler创建。然后从中提取出封装了异常信息的ServiceExceptionDetail对象,通过反射的方式重新创建异常对象。
注:在创建异常对象的时候,默认调用的是参数列表是String(Message)和Exception(InnerException)类型的公共构造函数,基本上绝大部分异常类型都具有这样的构造函数。如果某个异常不具有这样的构造函数签名,一般意味着并不希望异常对象从外部创建。比较典型的属SqlException,由于这样的异常只能通过System.Data.SqlClient数据存取提供者(Data Access Provier)创建,所以并不具有我们希望的构造函数。对于这种情况,在服务端必须利用替换机制将其替换成另一个可以创建的异常类型(比如将SqlException替换成自定义的DbException)。
3、通过行为应用自定义ErrorHandler和MessageInspector
我们上面创建的自定义ErrorHandler(ServiceErrorHandler)和MessageInspector(ExceptionHandlingMessageInspector),最终通过相应的WCF行为将它们分别应用到WCF服务端和客户端运行时。我们可以采用4种行为(操作行为、契约行为、终结点行为和服务行为)中的任何一种来实现,他们之间唯一的不同就是应用的方式(自定义特性或者配置)和作用范围不同。为此,我们创建了一个行为类型:ExceptionHandlingBehaviorAttribute:
1: using System; 2: using System.Collections.ObjectModel; 3: using System.ServiceModel; 4: using System.ServiceModel.Channels; 5: using System.ServiceModel.Description; 6: using System.ServiceModel.Dispatcher; 7: namespace Artech.EnterLibIntegration.WcfExtensions 8: { 9: public class ExceptionHandlingBehaviorAttribute:Attribute,IOperationBehavior,IContractBehavior,IEndpointBehavior,IServiceBehavior 10: { 11: public string ExceptionPolicyName 12: { get; private set; } 13: 14: public ExceptionHandlingBehaviorAttribute(string exceptionPolicyName) 15: { 16: if (string.IsNullOrEmpty(exceptionPolicyName)) 17: { 18: throw new ArgumentNullException("exceptionPolicyName"); 19: } 20: this.ExceptionPolicyName = exceptionPolicyName; 21: } 22: 23: #region IOperationBehavior Members 24: public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) {} 25: public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) 26: { 27: clientOperation.Parent.MessageInspectors.Add(new ExceptionHandlingMessageInspector()); 28: } 29: public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) 30: { 31: dispatchOperation.Parent.ChannelDispatcher.ErrorHandlers.Add(new ServiceErrorHandler(this.ExceptionPolicyName)); 32: dispatchOperation.Parent.ChannelDispatcher.ErrorHandlers.Add(new ServiceErrorHandler(this.ExceptionPolicyName)); 33: } 34: public void Validate(OperationDescription operationDescription) {} 35: #endregion 36: 37: #region IEndpointBehavior Members 38: public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) 39: {} 40: public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) 41: { 42: clientRuntime.MessageInspectors.Add(new ExceptionHandlingMessageInspector()); 43: } 44: public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {} 45: public void Validate(ServiceEndpoint endpoint) {} 46: #endregion 47: 48: #region IServiceBehavior Members 49: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection
在ApplyClientBehavior方法中,创建自定义的ExceptionHandlingMessageInspector对象,并将其加入ClientRuntime的MessageInspector列表中;在ApplyDispatchBehavior中,创建自定义的ServiceErrorHandler对象,并将其加入到ChannelDispatcher的ErrorHandler列表中。由于ExceptionHandlingBehaviorAttribute既是操作行为,又是契约行为和服务行为,同时又是一个自定义特性,所以我们可以直接通过特性的方式将其应用到操作方法、契约接口或者类型和服务类型上面。在下面的代码中,我们将其应用到服务契约的Divide操作上面:
同时作为服务行为和终结点行为,我们又具有另外一种服务应用的方式:配置。如果要实现通过配置方式应用该行为,我们还需要定义对应的继承自System.ServiceModel.Configuration.BehaviorExtensionElement类型的配置元素(Configuraiton Element)。为此,定义了如下一个类型:ExceptionHandlingBehaviorElement。
1: using System; 2: using System; 3: using System.Configuration; 4: using System.ServiceModel.Configuration; 5: namespace Artech.EnterLibIntegration.WcfExtensions 6: { 7: public class ExceptionHandlingBehaviorElement:BehaviorExtensionElement 8: { 9: [ConfigurationProperty("exceptionPolicy")] 10: public string ExceptionPolicy 11: { 12: get 13: {return this["exceptionPolicy"] as string;} 14: set 15: { this["exceptionPolicy"] = value; } 16: } 17: public override Type BehaviorType 18: { 19: get { return typeof(ExceptionHandlingBehaviorAttribute); } 20: } 21: protected override object CreateBehavior() 22: { 23: return new ExceptionHandlingBehaviorAttribute(this.ExceptionPolicy); 24: } 25: } 26: }
这样,我们就可以通过配置的方式将其应用到某个服务(作为服务行为)或者终结点(作为终结点行为)上了。在下面的配置中,我将此行为应用到CalculatorService服务上面。
三、 实例演示
接下来我们我们将上面我们定义的行为应用到真正的实例之中,看看它们是否会按照我们之前希望的方式进行异常的处理。简单起见,我们还是用我们熟悉的计算服务的例子。现在,我们将ExceptionHandlingBehaviorAttribute作为操作行为应用到服务契约接口ICalculator的Divide操作方法上,并指明异常处理策略名称(myExceptionPolicy)。
细心的读者可能注意到了:在Divide操作上面还同时应用了FaultContractAttribute特性,并将ServiceExceptionDetail类型作为错误明细类型。这样做的目的在于:用于封装异常信息的ServiceExceptionDetail类型必须作为错误契约,其对象才能被FaultFormatter序列化和反序列化。所以,将ServiceExceptionDetail作为错误契约时必须的。下面是服务类型的代码:
接下来,我们利用微软企业库提供的配置工具(Configuration Console)定义如下的异常处理策略,并命名为在ExceptionHandlingBehaviorAttribute指定的名称:myExceptionPolicy。此策略专门针对在Divide操作中会跑出的DivideByZeroException异常类型。
1: 2:
该异常策略定义非常简单,仅仅是将DivideByZeroException异常封装成我们自定义的CalculationException异常(封装后,原来的DivideByZeroException异常将会作为CalculationException异常的InnerException),并指定异常消息("计算错误")。CalculationException仅仅是一个普通的自定义异常:
1: using System; 2: namespace Artech.WcfServices.Contracts 3: { 4: [global::System.Serializable] 5: public class CalculationException : Exception 6: { 7: public CalculationException() { } 8: public CalculationException(string message) : base(message) { } 9: public CalculationException(string message, Exception inner) : base(message, inner) { } 10: protected CalculationException( 11: System.Runtime.Serialization.SerializationInfo info, 12: System.Runtime.Serialization.StreamingContext context) 13: : base(info, context) { } 14: } 15: }
最后,下面是客户端的代码,运行我们的应用程序,客户端将会得到如下的输出。
1: using System; 2: using System.ServiceModel; 3: using Artech.WcfServices.Contracts; 4: using Artech.EnterLibIntegration.WcfExtensions; 5: namespace Artech.WcfServices.Clients 6: { 7: class Program 8: { 9: static void Main(string[] args) 10: { 11: using (ExceptionHandlingChannelFactory
输出结果:
计算错误InnerException Type:System.DivideByZeroException Message:试图除以零.
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~