Attribute在.NET编程的应用(六)
(承上节) .NET Framework拦截机制的设计中,在客户端和对象之间,存在着多种消息接收器,这些消息接收器组成一个链表,客户端的调用对象的过程以及调用返回实行拦截,你可以定制自己的消息接收器,把它们插入了到链表中,来完成你对一个调用的前处理和后处理。那么调用拦截是如何构架或者说如何实现的呢?
在.NET中有两种调用,一种是跨应用域(App Domain),一种是跨上下文环境(Context),两种调用均通过中间的代理(proxy),代理被分为两个部分:透明代理和实际代理。透明代理暴露同对象一样的公共入口点,当客户调用透明代理的时候,透明代理把堆栈中的帧转换为消息(上一节提到的实现IMessage接口的对象),消息中包含了方法名称和参数等属性集,然后把消息传递给实际代理,接下去分两种情况:在跨应用域的情况下,实际代理使用一个格式化器对消息进行序列化,然后放入远程通道中;在跨上下文环境的情况下,实际代理不必知道格式化器、通道和Context拦截器,它只需要在向前传递消息之前对调用实行拦截,然后它把消息传递给一个消息接收器(实现IMessageSink的对象),每一个接收器都知道自己的下一个接收器,当它们对消息进行处理之后(前处理),都将消息传递给下一个接收器,一直到链表的最后一个接收器,最后一个接收器被称为堆栈创建器,它把消息还原为堆栈帧,然后调用对象,当调用方法结果返回的时候,堆栈创建器把结果转换为消息,传回给调用它的消息接收器,于是消息沿着原来的链表往回传,每个链表上的消息接收器在回传消息之前都对消息进行后处理。一直到链表的第一个接收器,第一个接收器把消息传回给实际代理,实际代理把消息传递给透明代理,后者把消息放回到客户端的堆栈中。从上面的描述我们看到穿越Context的消息不需要格式化,CLR使用一个内部的通道,叫做CrossContextChannel,这个对象也是一种消息接收器。
有几种消息接收器的类型,一个调用拦截可以在服务器端进行也可以在客户端进行,服务器端接收器拦截所有对服务器上下文环境中对象的调用,同时作一些前处理和后处理。客户端的接收器拦截所有外出客户端上下文环境的调用,同时也做一些前处理和后处理。服务器负责服务器端接收器的安装,拦截对服务器端上下文环境访问的接收器称为服务器上下文环境接收器,那些拦截调用实际对象的接收器是对象接收器。通过客户安装的客户端接收器称为客户端上下文环境接受器,通过对象安装的客户端接收器则称为特使(Envoy)接收器,特使接收器仅拦截那些和它相关的对象。客户端的最后一个接收器和服务器端的第一个接收器是CrossContextChannel类型的实例。不同类型的接收器组成不同的段,每个段的端点都装上称为终结器的接收器,终结器起着把本段的消息传给下一个段的作用。在服务器上下文环境段的最后一个终结器是ServerContextTerminatorSink。如果你在终结器调用NextSink,它将返回一个null,它们的行为就像是死端头,但是在它们内部保存有下一个接收器对象的私有字段。
我们大致介绍了.NET Framework的对象调用拦截的实现机制,目的是让大家对这种机制有一个认识,现在是实现我们代码的时候了,通过代码的实现,你可以看到消息如何被处理的过程。首先是为我们的程序定义一个接收器CallTraceSink:
//TraceContext.cs
using System;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Activation;
namespace NiwalkerDemo
{
public class CallTraceSink : IMessageSink //实现IMessageSink
{
private IMessageSink nextSink; //保存下一个接收器
//在构造器中初始化下一个接收器
public CallTraceSink(IMessageSink next)
{
nextSink=next;
}
//必须实现的IMessageSink接口属性
public IMessageSink NextSink
{
get
{
return nextSink;
}
}
//实现IMessageSink的接口方法,当消息传递的时候,该方法被调用
public IMessage SyncProcessMessage(IMessage msg)
{
//拦截消息,做前处理
Preprocess(msg);
//传递消息给下一个接收器
IMessage retMsg=nextSink.SyncProcessMessage(msg);
//调用返回时进行拦截,并进行后处理
Postprocess(msg,retMsg);
return retMsg;
}
//IMessageSink接口方法,用于异步处理,我们不实现异步处理,所以简单返回null,
//不管是同步还是异步,这个方法都需要定义
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
{
return null;
}
//我们的前处理方法,用于检查库存,出于简化的目的,我们把检查库存和发送邮件都写在一起了,
//在实际的实现中,可能也需要把Inventory对象绑定到一个上下文环境,
//另外,可以将发送邮件设计为另外一个接收器,然后通过NextSink进行安装
private void Preprocess(IMessage msg)
{
//检查是否是方法调用,我们只拦截Order的Submit方法。
IMethodCallMessage call=msg as IMethodCallMessage;
if(call==null)
return;
if(call.MethodName=="Submit")
{
string product=call.GetArg(0).ToString(); //获取Submit方法的第一个参数
int qty=(int)call.GetArg(1); //获取Submit方法的第二个参数
//调用Inventory检查库存存量
if(new Inventory().Checkout(product,qty))
Console.WriteLine("Order availible");
else
{
Console.WriteLine("Order unvailible");
SendEmail();
}
}
}
//后处理方法,用于记录订单提交信息,同样可以将记录作为一个接收器
//我们在这里处理,仅仅是为了演示
private void Postprocess(IMessage msg,IMessage retMsg)
{
IMethodCallMessage call=msg as IMethodCallMessage;
if(call==null)
return;
Console.WriteLine("Log order information");
}
private void SendEmail()
{
Console.WriteLine("Send email to manager");
}
}
...
接下来我们定义上下文环境的属性,上下文环境属性必须根据你要创建的接收器类型来实现相应的接口,比如:如果创建的是服务器上下文环境接收器,那么必须实现IContributeServerContextSink接口。
...
public class CallTraceProperty : IContextProperty, IContributeObjectSink
{
public CallTraceProperty()
{
}
//IContributeObjectSink的接口方法,实例化消息接收器
public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next)
{
return new CallTraceSink(next);
}
//IContextProperty接口方法,如果该方法返回ture,在新的上下文环境中激活对象
public bool IsNewContextOK(Context newCtx)
{
return true;
}
//IContextProperty接口方法,提供高级使用
public void Freeze(Context newCtx)
{
}
//IContextProperty接口属性
public string Name
{
get { return "OrderTrace";}
}
}
...
最后是ContextAttribute ...
[AttributeUsage(AttributeTargets.Class)]
public class CallTraceAttribute : ContextAttribute
{
public CallTraceAttribute():base("CallTrace")
{
}
//重载ContextAttribute方法,创建一个上下文环境属性
public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new CallTraceProperty());
}
}
}
为了看清楚调用Order对象的Submit方法如何被拦截,我们稍微修改一下Order类,同时把它设计为ContextBoundObject的派生类:
//Inventory.cs
//Order.cs
using System;
namespace NiwalkerDemo
{
[CallTrace]
public class Order : ContextBoundObject
{
...
public void Submit(string product, int quantity)
{
this.product=product;
this.quantity=quantity;
}
...
}
}
客户端调用代码:
...
public class AppMain
{
static void Main()
{
Order order1=new Order(100);
order1.Submit("Item1",150);
Order order2=new Order(101);
order2.Submit("Item2",150);
}
}
...
运行结果表明了我们对Order的Sbumit成功地进行了拦截。需要说明的是,这里的代码仅仅是作为对ContextAttribute应用的演示,它是粗线条的。在具体的实践中,大家可以设计的更精妙。
后记:本来想对Attribute进行更多的介绍,发现要讲的东西实在是太多了。请允许我在其他的专题中再来讨论它们。十分感谢大家有耐心读完这个系列。如果这里介绍的内容在你的编程生涯有所启迪的话,那么就是我的莫大荣幸了。再一次谢谢大家。
(全文完)