A Taste of AOP from Solving Problems with OOP and Design Patterns (Part III) zz
Posted on 2008-04-06 18:10 [虫子] 阅读(444) 评论(0) 编辑 收藏 举报在本文的上一篇中,我们利用.NET Remoting基础架构中的真实代理/透明代理技术实现了不针对具体类型、具体方法的通用方法调用拦截机制。由于技术内容太多,本来想一同写在上一篇中的最后一大块内容就新作一篇吧。在本篇文字中,我们介绍可以用于在.NET中实现基本AOP(面向方面编程)的更深入的技术,并结合该技术的优势和劣势提出了一个已经在我们设计的项目中应用的一个AOP框架原型思路。
在前一篇文字的结尾,我们提出了几点技术方案中感觉不爽的环节,其中群众呼声最大的就是觉得对象的构造不够intuitive:一方面需要引入FACTORY METHOD(如CreateInstance()),另一方面在该方法中的构造过程也不够simple(proxied real object、real proxy、GetTransparentProxy……),还有,由于我们是在构造proxied对象之后才将方法拦截代理(即真实代理)施加在真实对象上,因此我们自然无法拦截到对对象构造器的方法调用——这个不足对于很多功能实现场合都是致命的(比如说需要监视系统中各类应用对象的创建频率,我们就需要在构造器调用时进行相应计数逻辑;另外对对象构造参数的记录和分析也是很常用到的)。在本文中,我们就引入新的技术来一一解决这些问题吧!
首先,有没有更简单的办法来构造被代理的对象呢?有没有办法在构造对象之前就设置好消息拦截器呢?我们来考虑这行代码:
Calc calc = new Calc();
这可以算是最简单的创建对象的写法了吧!如果我们能够覆盖new指令的默认行为使之利用我们自己的FACTORY就好了——可惜,CLR/C#都不支持覆盖(override)new操作符。看来此路不通……可是——我突然想到了在.NET Remoting中是可以用new操作符直接构造远程对象的(而远程对象不正是一个经过代理的对象吗)!只要Calc是MarshalByRefObject类型且在Remoting中正确的配置为wellknown type的话,则当CLR遇到C#中的new指令(也即IL中的newobj指令)时,它将把对象激活的任务交给.NET Remoting来完成。比如这样:
public class Calc: MarshalByRefObject {…}
RemotingConfiguration.RegisterWellKnownClientType(typeof(Calc),
"tcp://remoteServer:8080/calcService");
Calc calc = new Calc();
Assert.IsTrue(RemotingServices.IsTransparentProxy(calc));
你可以自己试试这种写法,看样子还真的有戏——只要我们能够实现一个和对象激活地址(URL)配套的channel在指定位置监听由Remoting发来的方法调用消息并处理就可以!只是……这不是有点儿添乱吗?
再想想有什么线索……System.EnterpriseServices里面的ServicedComponent!因为它是为了让.NET的对象可以使用COM+提供的组件服务,那么它也应该应用了透明代理技术。让我们来验证一下(记得Add Reference to System.EnterpriseServices assembly and use this namespace!):
public class Calc: ServicedComponent {…}
Calc calc = new Calc();
Assert.IsTrue(RemotingServices.IsTransparentProxy(calc));
Console.WriteLine(RemotingServices.GetRealProxy(calc).GetType());
当然,为了在一个assembly里面实现ServicedComponent,我们首先要给assembly一个strong name,原理就不多说,只要简单的为所在assembly施加这么一个自定义属性即可:
[assembly: System.Reflection.AssemblyKeyFile(@"C:\xxx.snk")]
其中这个C:\xxx.snk文件是事先用.NET SDK的sn工具生成的(如:sn -k C:\xxx.snk)。编译执行,发现果然如此!此时新创建的calc实例确实是一个经过透明代理包装的对象,而且这个透明代理所对应的真实代理是System.EnterpriseServices里面的一个叫ServicedComponentProxy的类(注意看Console.WriteLine()方法的输出结果)。意思对了,可是感觉有点儿“过”,还是先来研究一下ServicedComponent到底有什么名堂吧——为什么一个类从它派生出来以后再new的时候就被套上了一个代理呢?
原来,ServicedComponent是从System.ContextBoundObject派生而来的,而这个类又是从MarshalByRefObject派生而来。看来,MarshalByRefObject不能满足我们的需要,而ServicedComponent又是为了一个特定的开发需求而设置的基类,那么其中夹着的这个ContextBoundObject就应该是一个提供某种公共特性的基类啦!赶快来印证一下,把Calc的基类变成这个ContextBoundObject:
public class Calc: ContextBoundObject {…}
再运行前面的同样的测试代码……噻!就是它了!看来只要一个类是从ContextBoundObject派生而来(以后我们也用CBO来简称这类对象),那么当它被new的时候,就会被套上一个透明代理——而这个代理背后干活的真实代理类(再次注意看Console.WriteLine()输出的透明代理的真实代理的类型!)就是System.Runtime.Remoting.Proxies名称空间中的RemotingProxy(这是一个internal的类型,所以既没有文档也不能直接被使用——事实上,这个真实代理就是实现.NET Remoting所有核心机制与扩展机制的类!)。
关于CBO的详细用途和使用方法,还请读者自己查一下MSDN文档(趁有得看还是看看吧——再往后一些的内容可就想看也没有喽!),我这里只是大概讲解一下。ContextBoundObject,顾名思义,object that will be bound with a context。这里的所谓context是指一个逻辑上的执行环境,每一个AppDomain中都有一个或多个这样的context,就好比每一个托管进程都可以有一个或多个AppDomain一样。AppDomain中的第一个context也叫做default context,所有不是CBO的对象(也称为context-agile object)默认都是在这个context中创建和运行。而CBO对象都将创建并运行在自己所需要或认可的context中。跨越context的方法调用都是通过透明代理转发的,因为context所设立的执行环境是由若干个对象用法规则所保障的,只有通过代理机制才可以通过设置方法拦截器来对进出context的方法进行规则保障(这和COM/COM+中的概念是相似的,COM+的所有服务也都是通过类似的方法拦截代理来实现的——其实更准确的说应该是.NET CLR大量借鉴了COM+的设计与实现机制)。
执行线程所在的context可以通过System.Threading名称空间中的Thread.CurrentContext取得,它的类型是System.Runtime.Remoting.Contexts名称空间中的Context类,其中每个Context实例都有一个唯一的ID(ContextID)。前面说的default context也是这个Context类上面的一个静态属性(DefaultContext)。我们可以通过它来观察在一个线程上跨越多个对象时每个方法调用所发生在的逻辑context。比如这样(记得using System.Threading!):
public class Calc: ContextBoundObject
{
public Calc()
{
Console.WriteLine("Calc(): " + Thread.CurrentContext);
}
public int Add(int x, int y)
{
Console.WriteLine("Add: " + Thread.CurrentContext);
return x + y;
}
}
public class Application
{
public static void Main()
{
Console.WriteLine("Main: " + Thread.CurrentContext);
Assert.AreEqual(3, new Calc().Add(1, 2));
}
}
你可以看到,对Calc实例的方法调用是发生在另一个context内的(作为对比,你可以把Calc声明为从MarshalByRefObject或Object再看看)。(最好找个开发环境动手实验一下——笔者四月一日注;)
除了ContextID之外,Context对象上的另一个重要概念是context property。在Context类中就有一个叫ContextProperties的属性,它的类型是一个IContextProperty[]。那么到底什么是context property呢?这可以是任何一个实现了IContextProperty接口的对象,这个对象可以为其相关的context提供一些特性(这里我把property称为“特性”,因为如果叫成“属性”有可能会和attribute混淆——而还真就有一个叫context attribute的东西!这个我们稍后就会遇到)。而IContextProperty接口中只定义了一个Name属性,还有几个方法,我们稍后再说。除了提供只读的数组之外,可以看到Context对象还提供了GetProperty()和SetProperty()两个方法。如果感兴趣可以动手试试,你会发现一上来每个context就有一个叫LeaseLifeTimeServiceProperty的东东,而用它的名字去GetProperty()也能取得,但是一SetProperty()就会出错——就像对象的property一样,context的property虽然不是类型的一部分,但是也只能在context构造的时期形成并在context的生存期内一直存在。如何为context指定一些特性呢?我们需要用context attribute(在.NET中一般说到attribute都是指可以通过反射获取的attribute)来把所需的context property设置在context中,而因为context是为ContextBoundObject来提供合适的执行环境的,所以这些context attribute就自然是附着在CBO派生类型身上了,就像这样:
[SomeContextAttribute]
public class AnyContextBoundObject: ContextBoundObject {…}
那么context attribute如何来将指定的context property提供给context呢?实际上,一个context attribute是一个实现了IContextAttribute接口的Attribute(什么?属性也可以实现接口?以后你将越来越多的看到这种机制在.NET中的灵活运用),它将在CBO对象被构造的阶段被反射实例化。随后,CLR将通过这个IContextAttribute接口与context attribute进行交互,以获知对象所需要的合适的执行环境(也就是由context properties所确定的一个具体的context)。为了做到这一点,IContextAttribute中定义了下面这两个方法:
public interface IContextAttribute
{
bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg);
void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg);
}
每个context attribute在context的构造阶段(通常是由CBO对象构造动作引发的)会被首先问到IsContextOK,就是说新创建的这个CBO(通过ctorMsg可以知道是哪个对象的哪个构造方法被用来构造CBO对象的)能不能在给定的ctx中存在呢?这个目的主要是减少AppDomain中潜在的context的数量,如果某些CBO类型可以共用一个有所需特性的执行环境的话,就可以不用再创建新的环境,而只要在已有的环境中构造并执行就好了。
如果CBO类型上设置的所有context attributes都认同给定的context(也即调用代码所处的context)是okay的(即IsContextOK均返回true),那么新的CBO就会被绑定到这个context上。否则,只有有一个属性投了否决票,就马上会有一个新的context被创建出来。紧接着CLR会再一次询问每一个context attribute这次新构造的context是否okay,这时候大部分都会说no了——毕竟是重新开始了嘛。因此在这个阶段,context构造程序将对每一个认为新的ctx不okay的context attributes调用其GetPropertiesForNewContext()方法——其实这个方法叫做SetPropertiesForNewContext()可能会更贴切,context attribute可以用这个方法传入的构造器方法调用信息(ctorMsg)中的context properties列表(ContextProperties)来为新建的context增加所需的context properties!
话说多了,还是写些代码来看看吧!假如说我们要求Calc对象所在的Context都必须有一个叫CallLogger的特性(先不用管这个特性到底能够干什么),我们会这样把它指定在Calc类上:
[CallLogger]
public class Calc: ContextBoundObject {…}
而这样我们就要有一个叫CallLoggerAttribute的context attribute:
public class CallLoggerAttribute: Attribute, IContextAttribute
{
public void IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
{
return false;
}
public void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new CallLoggerProperty());
}
}
注意这里我们首先让IsContextOK()总返回false,使得Calc总会被放在一个新的环境中(不过在编写自己的context attribute的时候还是应该仔细考虑尽可能让多个对象共享已有的环境以减少创建新环境以及跨环境调用的开销)。随后,我们在GetPropertiesForNewContext()方法中使用传入的代表着CBO构造方法调用的消息(ctorMsg)的ContextProperties属性(这个属性的类型是IList,所以是可以Add/Remove的)来添加我们所需的环境特性,如CallLoggerProperty:
public class CallLoggerProperty: IContextProperty
{
public string Name { return "CALLLOGGER"; }
public bool IsNewContextOK(Context newCtx) { return true; }
public void Freeze(Context newCtx) {}
}
实现IContextProperty的时候主要就是要提供特性的Name(这个名字必须在整个context中是唯一的!),其他两个方法一个是用来在所有的环境特性都到位后再次确认彼此没有冲突的情况发生(IsNewContextOK),另一个则是通知context property:新的context即将构造完成,请进入冻结状态(Freeze——通常情况下并不需要采取什么动作)。
有了这些代码,我们来看看新构造的Calc实例是否执行在具有我们所需的环境特性的执行环境中:
[CallLogger]
public class Calc: ContextBoundObject
{
public int Add(int x, int y)
{
IContextProperty property = Thread.CurrentContext.GetProperty("CALLLOGGER");
Assert.IsNotNull(property);
Assert.IsTrue(property is CallLoggerProperty);
…
}
}
确实如我们所愿,通过为CBO派生类施加context attribute,我们就可以控制CBO类型构造和运行所需的执行环境,也就是设置该环境中的context properties,进而在该CBO实例的内部方法调用中通过Context.GetProperty()取得由context attribute设置好的context properties。挺好,都通了——可以有什么用呢?现在我们来忽悠一下想象的翅膀——context property不就是一个对象吗?如果它有自己的数据和方法,我们不就可以用它做点儿什么了吗?比如说:
public class CallLoggerProperty: IContextProperty
{
public void Log(string message)
{
Console.WriteLine(message);
}
…
}
那么我们在Calc中就可以使用环境中的这个特性啦:
[CallLogger]
public class Calc: ContextBoundObject
{
public int Add(int x, int y)
{
IContextProperty property = Thread.CurrentContext.GetProperty("CALLLOGGER");
CallLoggerProperty logger = property as CallLoggerProperty;
if (logger!=null) logger.Log(String.Format("Add({0},{1})", x, y));
int result = x + y;
if (logger!=null) logger.Log(String.Format("={0}", result));
}
}
靠!这不折腾吗?原来写一句话就能做的事情现在要绕这么大一个圈?!嘿嘿,不过至少我们现在可以自由的开关这个Log方法了吧?把[CallLogger]注释掉,程序照样转,就是没有Log功能了(因为logger==null)——可是毕竟Calc这个具体类要了解(或者说依赖)这个具体的CallLoggerProperty才可以用它进行Log操作,这种依赖性是很愚昧的(就像progame同学指出的那样——不过请注意,无论如何类的内部是不依赖于context attribute的!),如果.NET真的设计成这个样子那可真的是不可救药了……还好,在愤怒之前先冷静一下,因为我发现大部分事情都比我们想象的要科学的多(尤其在没有透彻理解之前),让我们来设想一下理想的情形:Calc根本不需要了解任何context attribute或者context property,它只需要完成它所需要做的;而既然Calc已经在一个context内部运行,跨越context的方法调用又是自动通过透明代理来进行的,那么应该有某种机制可以与透明代理合作的话,原本这些显式的代码就可以隐藏在后面了——而且我们在前面的文章中也反复强调了,透明代理这种基于消息的拦截机制是对类型和具体方法不敏感的,这也是它的一个重要特点,我们应该合理的加以利用。
这种机制是什么呢?还记得我们上一篇中讲过的IMessageSink吗?还记得我们利用一个链式代理将一串儿方法消息拦截器串接起来拦截并处理方法调用消息吗?没错,这正是.NET内部使用的链式方法拦截机制!在继续之前,让我们先来写一个CallLoggerSink,顺便复习一下:
public class CallLoggerSink: IMessageSink
{
private readonly IMessageSink _nextSink;
public CallLoggerSink(IMessageSink nextSink)
{
_nextSink = nextSink;
}
public IMessageSink NextSink
{
get { return _nextSink; }
}
public IMessageCtrl AsyncProcessMessage(…) {…}
public IMessage SyncProcessMessage(IMessage msg)
{
IMethodCallMessage callMsg = msg as IMethodCallMessage;
msg = NextSink.SyncProcessMessage(msg);
IMethodReturnMessage returnMsg = msg as IMethodReturnMessage;
LogMessageProcessing(callMsg, returnMsg);
}
protected virtual void LogMessageProcessing(IMethodCallMessage callMsg,
IMethodReturnMessage returnMsg)
{
string methodName = callMsg.MethodBase.Name;
string[] methodArgs = new string[callMsg.InArgCount];
for (int i=0; i<methodArgs.Length; i++)
{
methodArgs[i] = callMsg.InArgs[i].ToString();
}
string returnValue = returnMsg.ReturnValue;
Console.WriteLine("{0}({1})={2}", methodName,
String.Join(", ", methodArgs),
returnValue);
}
}
这一部分不多解释了,如果有不明白的地方可以参考前一篇文章中的相关内容。现在我们想做的事情是把这个通用的方法消息拦截器植入到任意context的透明代理中(实际上是被相应的真实代理所调用的),以形成一个具有特定功能的执行环境——这个工作正是由context property来做的!
实际上,一个context property类如果光实现IContextProperty的话基本上没有什么大用,它只是提供了作为一个环境特性的最基本的信息。要想通过context property向所在context的TP/RP中植入message sinks的话,还需要实现一个或多个IContributeXXXSink接口——这里的XXX可以有四种可能:Envoy、ClientContext、ServerContext、Object(其实还有一种是DynamicSink,不过跟着四个还不太一样,以后用得到的时候再说吧)。复杂了,看文档吧……更复杂了,文档说这些都是为了支持.NET框架基础结构而设计的,不是让用户代码来使用的。还是跟着我一起来研究吧!:)
首先一点,从名称上揣测:IContributeSomething,应该是“贡献”个什么东西。看看接口中定义的方法,还好,都只有一个GetXXXSink(),返回值都是IMessageSink,而参数也很简单,无外乎就是nextSink(显然是为了我们构造IMessageSink用的)或者外加一个MarshalByRefObject(是不是以前讲过的target呢?:)。实际上,这些都是所谓的FACTORY METHODS,而context property就是用来创建这一系列IMessageSink实现的工厂!可是何来这四种sink呢?不妨利用已有的知识实际研究一下吧!让我们来写一个简单的sink,并在每一个工厂方法中都返回这个类的新实例,然后来分析每一个工厂方法构造的sink都是什么用途:
public class ResearchSink: IMessageSink
{
private readonly IMessageSink _nextSink;
private readonly string _sinkKind;
public ResearchSink(string sinkKind, MarshalByRefObject target, IMessageSink nextSink)
{
_nextSink = nextSink;
_sinkKind = sinkKind;
Console.WriteLine("{0}Sink is created on {1}...", sinkKind, Thread.CurrentContext);
Console.WriteLine("\tTarget is {0}", target==null ? "<null>" : target);
Console.WriteLine("\tNextSink's type is {0}", nextSink.GetType());
}
public IMessageSink NextSink
{
get { return _nextSink; }
}
public IMessage SyncProcessMessage(IMessage msg)
{
IMethodCallMessage callMsg = msg as IMethodCallMessage;
Console.WriteLine("{0}Sink is being called to process msg for {1}.{2}()", _sinkKind, callMsg.MethodBase.DeclaringType, callMsg.MethodBase.Name);
return _nextSink.SyncProcessMessage(msg);
}
public IMessageCtrl AsyncProcessMessage(…) {…}
}
然后我们利用一个ResearchProperty贡献所有四种sink:
public class ResearchProperty: IContextProperty,
IContributeEnvoySink,
IContributeClientContextSink,
IContributeServerContextSink,
IContributeObjectSink
{
public string Name { get { return "RESEARCH"; } }
public bool IsNewContextOK(Context newCtx) { return true; }
public void Freeze(Context newCtx) {}
public IMessageSink GetEnvoySink(MarshalByRefObject obj, IMessageSink nextSink)
{
return new ResearchSink("Envoy", obj, nextSink);
}
public IMessageSink GetClientContextSink(IMessageSink nextSink)
{
return new ResearchSink("ClientContext", null, nextSink);
}
public IMessageSink GetServerContextSink(IMessageSink nextSink)
{
return new ResearchSink("ServerContext", null, nextSink);
}
public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
{
return new ResearchSink("Object", obj, nextSink);
}
}
再写一个对应的context attribute以便可以将这个特性施加到任何一个CBO类型的定义上:
public class ResearchAttribute: Attribute, IContextAttribute
{
public bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
{
return false;
}
public void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new ResearchProperty());
}
}
现在在Calc类型上面施加[Research]属性,并调用其Add()方法……看到了一连串的输出信息?感觉还不错吧!我们可以看到,除了ClientContext之外,所有其他的sink都被创建并调用过,而且在ServerContextSink的方法消息处理器中我们甚至收到了对Calc构造器的方法调用消息!实际上,上面提到的四种sink对应于跨环境调用对象方法的四个不同阶段:当一个CBO对象实例收到一个来自于其他环境的方法调用时,我们称这个对象为server object,而其所处的环境也即server context,那么.NET Remoting将在server context中先后调用已注册的ServerContextSink和ObjectSink的方法消息处理器(即SyncProcessMessage()方法);同理,当一个CBO对象向另一个环境中的CBO对象发出方法调用时,我们称这个发起调用的CBO对象为client object,而其所处的环境也即client context,那么.NET Remoting将先后调用已注册的EnvoySink和ClientContextSink的方法消息处理器——不过此时调用的EnvoySink并不是由client context贡献的,而是由server context贡献的,所以对于发起调用的client context而言它是server context的特使(envoy)!当然,ClientContextSink都是在client context中被调用的。
透彻理解.NET Remoting中提供的这些sink机制并不是本文要讲的重点,如果感兴趣的话可以参考MSDN中的一些文章(如果要求强烈我也会考虑在以后的文章中详细讲解几种sink的设计意图和应用场合:)。实际上,对这些sink的支持都是通过前面说到的RemotingProxy这个.NET Remoting中枢代理来实现的,所以对这些内容的探讨放在跟.NET Remoting相关的文章中会更合适。
不过利用我给出的研究工具,不难亲自体会这其中的设计奥秘,并想象如何利用这些技术手段来实现之前提出的若干关于AOP方面的问题。不过在本文,我还是想继续揭露更多的技术细节。在进行之前,我们先来回顾一下已经可以实现的特性。
首先,现在我们可以直接用C#语言提供的new指令构造一个CBO对象,并返回一个使用RemotingProxy作为真实代理的透明代理对象——这解决了前文中提出的构造麻烦的问题;其次,利用RemotingProxy中设立的种种机制,我们可以在定制CBO类型的执行环境,利用context attribute植入context property,并利用context property向执行环境中注入方法拦截器(IMessageSink)——这其实可以做到前文中的MessageChainProxy的功效;最后,因为建立执行环境是在构造访对象实例之前完成的,而方法拦截器又是在建立执行环境期间注入的,因此我们已经可以拦截到用来构造对象实例的构造器方法调用了——这也解决了前文中提到的功能缺憾。那么我们还可以做些什么呢?
经过一段时间的实践,我们发现如果只是需要通过拦截方法调用来实现AOP的一些基本开发任务的话,需要在创建和特定执行环境绑定的CBO对象的同时也创建很多的执行环境。同时,在不同执行环境之间进行方法调用也涉及大量的环境切换。这些开销实际上都是由于RemotingProxy为支持.NET Remoting而实现的复杂逻辑造成的。有没有办法能够用我们自己的真实代理替换默认的这个RemotingProxy以支持最小的用于实现AOP功能的基础机制呢?
回想当时对ServicedComponent的研究可以发现,当创建一个ServicedComponent派生对象的时候后,我们得到的透明代理的真实代理其实是一个叫ServicedComponentProxy的类(而不是RemotingProxy!)。这说明.NET肯定提供了某种可以替换真实代理的机制!利用.NET Reflector等工具,我们发现在ServicedComponent这个派生于ContextBoundObject的类型上被设置了一个ServicedComponentProxyAttribute自定义属性。这个属性又是派生于System.Runtime.Remoting.Proxy中的ProxyAttribute。在这个自定义属性类中,我们发现了一个virtual的CreateInstance()方法,看来这就是真正构造CBO对象实例的工厂方法喽!
原来,当我们构造一个CBO对象实例的时候,CLR将首先检查该类上是否被施加了从ProxyAttribute派生的自定义属性。如果有的话,CLR将实例化该自定义属性并调用其CreateInstance()方法获得CBO对象实例(MarshalByRefObject)。在这个方法中,我们就可以利用自己的真实代理生成透明代理,而不是用系统默认的过于强大的RemotingProxy!为了印证这个逻辑,我们可以用一个什么都不干的真实代理换上默认的RemotingProxy:
public class BypassProxy: RealProxy
{
private readonly MarshalByRefObject _target;
public BypassProxy(MarshalByRefObject target): base(target.GetType())
{
_target = target;
}
public override IMessage Invoke(IMessage msg)
{
return RemotingServices.ExecuteMessage(_target, (IMethodCallMessage)msg);
}
}
再写一个从ProxyAttribute派生的自定义代理属性,通过覆盖其CreateInstance()方法来构造给定的类型的实例,并套上由我们自己指定的BypassProxy生成的透明代理返回:
[AttributeUsage(AttributeTargets.Class)] // whose hack to force this!?
public class BypassProxyAttribute: ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
MarshalByRefObject rawInstance = base.CreateInstance(serverType);
RealProxy realProxy = new BypassProxy(rawInstance);
return (MarshalByRefObject)realProxy.GetTransparentProxy();
}
}
在这段代码中,我们首先使用基类的CreateInstance()方法来构造所需的serverType的“实例”(为什么不能用Activator.CreateInstance()呢?请自己想一想,或者动手试一试!),然后一如既往的构造我们自己的真实代理,传入刚刚构建好的目标对象实例,然后从中生成真实代理,并将其返回。再继续之前请您先想一想这段代码是否可以工作——注意new BypassProxy(rawInstance)这句话!如果运行这段代码,你将得到一个RemotingException,说“Trying to call proxy while constructor call is in progress.”。这是为什么呢?原来,我们在BypassProxy的构造器中调用了传入的target对象的GetType()方法来得到其运行时类型。这在以前一直工作的很好,然而在此时,我们正处于BypassProxyAttribute的CreateInstance()方法内部,虽然已经用base.CreateInstance()构造了对象实例,但是整个构造过程还没有完成!因此这时候还不允许调用对象的任何实例方法。换句话说,刚刚得到的rawInstance对象只是一个刚刚内部初始化完成但还尚未执行构造方法调用的萌芽状态(所以我叫它rawInstance嘛)。解决这个问题很简单,只需要在构造真实代理的时候显式传入serverType即可:
public class BypassProxy: RealProxy
{
…
public BypassProxy(MarshalByRefObject target, Type targetType):
base(targetType)
{
_target = target;
}
}
[AttributeUsage(AttributeTargets.Class)]
public class BypassProxyAttribute: ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
…
RealProxy realProxy = new BypassProxy(rawInstance, serverType);
…
}
}
这次再运行,我们又将遇到一个新的异常(仍旧是RemotingException):“ExecuteMessage can be called only from the native context of the object.”。这又是什么鸟?调试一下发现,原来是已经走到了BypassProxy.Invoke(),而此时我们收到了一个ConstructorCallMessage(实现的接口则是IConstructionCallMessage——要是没有IntelliSense这日子还真不好过的说)!这不正是对象构造器方法调用消息嘛!这是我们一直希望能够拦截到的一个关键事件。然而真的拦到了我们又该如何处理它呢?注意到RealProxy基类中有一个叫做InitializeServerObject()的方法,它可以接受一个IConstructionCallMessage并返回一个IConstructionReturnMessage,我们可以用它来处理构造器方法调用消息:
public class BypassProxy: RealProxy
{
public override IMessage Invoke(IMessage msg)
{
if (msg is IConstructionCallMessage)
return base.InitializeServerObject((IConstructionCallMessage)msg);
…
}
}
好了,有了这个秘密武器,我们期待的结果就可以被断言了:
[BypassProxyAttribute]
public class AnyCBO: ContextBoundObject {}
object instance = new AnyCBO();
Assert.IsTrue(RemotingServices.IsTransparentProxy(instance));
Assert.AreEqual(typeof(BypassProxy), RemotingServices.GetRealProxy(instance));
不过不要高兴得太早,离通关还远着呢(学习和研究真的就像玩游戏一样的说)!虽然目前一切看着都很顺利,我们也能够用最简洁的办法拿到一个被代理的对象,可是除了构造阶段我们能够拦截到构造器方法调用之外(爽),以前能够拦截到的普通方法却怎么也无法正常拦截到了(衰)!真是岂有此理!莫名其妙!这也是我在研究这套技术时候遇到的最匪夷所思的事情。这里我真的希望你也自己动手找找解决方法,分析一下个中原因——也算对得起我的付出吧(兄弟!怎么着也花个半天儿一天儿的吧!我写这几篇文章可是用了两个多月的零散时间了!)。
所谓以毒攻毒,为了对付这个怪物,我们要用到另一个怪物:System.Runtime.Remoting.Services名称空间中的EnterpriseServicesHelper。说它是怪物一点儿也不过分:就三个静态方法,却忘了藏起自己的默认构造器(这样的类在C# 2.0中都应该标记为static才符合编码准则);三个静态方法也都没做什么事儿——都是把一些internal的事情public出来而已;最烦的是还把自己放到一个冠冕堂皇的名称空间中……不过很多时候就是靠怪物的伎俩才能通关的说:我们得用它的CreateConstructionReturnMessage()方法来构造一个实现了IConstructionReturnMessage的internal的ConstructorReturnMessage类。为什么呢?原因是前面用到的RealProxy的InitializeServerObject()方法返回的IConstructionReturnMessage里面包含的仍是未经过代理的原始对象实例——也就是说,虽然我们在ProxyAttribute的CreateInstance()中把一个处于萌芽状态的原始对象实例传给了RealProxy,也因此拦截到了第一手构造器方法调用,然而我们在处理这个构造器方法调用的时候却还是返回了一个赤裸裸的原始对象实例给构造对象的调用代码!只是直到现在我还在奇怪为什么我们利用RemotingServices的GetRealProxy()方法还是能够得到这个赤裸裸的原始对象实例的真实代理……废话少说,来看看我们需要写哪些代码来最终实现我们的理想:
public class BypassProxy: RealProxy
{
public override IMessage Invoke(IMessage msg)
{
if (msg is IConstructionCallMessage)
{
IConstructionCallMessage ctorMsg = (IConstructionCallMessage)msg;
RemotingServices.GetRealProxy(_target).InitializeServerObject(ctorMsg);
return EnterpriseServicesHelper.CreateConstructionReturnMessage
(ctorMsg, (MarshalByRefObject)this.GetTransparentProxy());
}
…
}
}
怎么看怎么变态:我们首先需要使用之前ProxyAttribute.CreateInstance()创建的处于萌芽状态的目标对象的真实代理来按照构造器方法调用消息(ctorMsg)初始化;然后我们利用EnterpriseServicesHelper的CreateConstructionReturnMessage()方法构造一个构造器返回消息,而消息中用于返回给调用代码的对象则是我们自己的真实代理生成的透明代理(本可以不写this,这里我是希望强调这两步中真实代理主体的差异)。
无论如何,现在我们已经基本上走通了所有的技术难点:我们可以用new来直接构造一个经过代理的对象(只要对象派生自ContextBoundObject);我们可以利用ProxyAttribute派生属性来为任意CBO类指派自定义真实代理(覆盖ProxyAttribute基类中的CreateInstance()方法);我们继而可以在自定义的真实代理中拦截到包括构造器方法调用在内的所有发往目标对象上的方法调用消息——而最重要的是,我们现在无需切换执行环境即可为目标对象添加方法级的服务代码(这是相比使用context property机制最大的优势)——现在我们只需要提供一个同样可以利用IMessageSink机制的定制真实代理就可以完成一个最基本的AOP开发框架了!
这个真实代理就是前文中讲过的MessageChainProxy,只是我们需要为其增加处理IConstructionCallMessage的处理分支就可以了。再结合一些简单的自定义属性,我们就可以编写这样的调用代码,来利用这个通用的AOP框架:
[AopProxy]
public abstract class ObjectWithAspects {}
[CallLogger, ExceptionPublisher, PerformanceMoniter, PersistManager]
public class AnyBusinessObject: ObjectWithAspects {…}
AnyBusinessObject instance = new AnyBusinessObject();
instance.DoBusiness(…);
这里,AopProxyAttribute就是一个提供自定义真实代理的ProxyAttribute,基本代码示意如下:
[AttributeUsage(AttributeTargets.Class)]
public class AopProxyAttribute: ProxyAttribute
{
private Attribute[] GetAspectAttributes(Type classType)
{
return classType.GetCustomAttributes(typeof(AspectAttribute), true);
}
public override MarshalByRefObject CreateInstance(Type serverType)
{
MarshalByRefObject rawInstance = base.CreateInstance(serverType);
IMessageSink headSink = new TerminatorSink(rawInstance);
foreach (AspectAttribute aspectAttribute in GetAspectAttributes(serverType))
{
headSink = aspectAttribute.CreateAspectSink(headSink);
}
RealProxy aopProxy = new MessageChainProxy(serverType, rawInstance, headSink);
return (MarshalByRefObject)aopProxy.GetTransparentProxy();
}
}
而以CallLoggerAttribute为例,它其实是一个拦截器工厂,通过从一个公共的基类派生并实现工厂方法向AopProxyAttribute的CreateInstance()方法提供拦截器实例:
public abstract class AspectAttribute: Attribute
{
public abstract IMessageSink CreateAspectSink(IMessageSink nextSink);
}
public class CallLoggerAttribute: AspectAttribute
{
public override IMessageSink CreateAspectSink(IMessageSink nextSink)
{
return new CallLoggerSink(nextSink);
}
}
而CallLoggerSink则是一个标准的完全可以重用的IMessageSink实现——这种结构的美在于我们是在针对公共接口编程,所以同样的实现可以应用于了解这个公共接口的所有机制中(比如这些IMessageSink也可以不加改动直接用context property将其contribute到任意的context中)。
当然,如果你需要在代码被部署之后可以灵活通过配置文件来改变施加在不同对象上的aspect对象,比如:
<aopSetting>
<objectPattern typeNameRegex="…">
<applyAspect type="My.Aspects.CallLogger" />
</objectPattern>
<objectPattern fromNamespace="My.Business.Services">
<applyAspect type="My.Aspects.PerformanceMoniter" />
</objectPattern>
<objectPattern fromNamespace="My.Business.Entities">
<applyAspect type="My.Aspects.PersistManager" />
</objectPattern>
</aopSetting>
则只需要在AopProxyAttribute中加入相应的逻辑即可。本文中提到的机制已经以某种形式运用在我最近设计的企业级项目中,其可行性、可用性已经得到了初步的检验,不过距离一个完美的AOP框架肯定还有很长的路要走。不过也正如本文题目所言,这里我只是希望让大家尝尝鲜(a taste of而已),先接触一下AOP的概念,并对在.NET中实现AOP的几种基本技术有一个初步的了解。从这个层面上说,我想我的目的已经达到了——很多很多朋友仔细的读过这些文章之后给我发来邮件探讨相关的技术问题,有些朋友主动的利用我介绍的这些技术解决一些以前不能很好解决的问题,更有朋友已经在考虑利用各种技术来实现一个面向应用的AOP框架for .NET……这正是我非常希望看到的局面:技术要运用起来才能体现出价值。
事实上,为什么要写这一系列文章呢?其实本来就是希望写一些与这些技术相关的具体运用,可是发现了解这些技术的朋友实在是太少,有必要先铺垫一下,所以就引出来这一旷日持久的AOP尝鲜系列文章。聪明的你一定已经意识到,本文中介绍的几种技术完全还有更广阔的应用领域——比如我之前提过的mock object和ElegantDAL interpreter等等……这些话题我稍后就会涉及,希望到时候能够把我的心得与大家分享,并期望听到更多的反馈。
按照惯例,我的文章都不配代码(甚至文中的代码我都不是copy-and-paste来的,所以你也不要copy-and-paste回去——我不能保证完全正确,但我还是会尽量写出最贴切的示意性代码),我鼓励大家动手实践,并期待听到你的反馈(比如说:几种机制在性能方面你有什么发现?在功能运用上你有什么体会?感觉还有哪些不妥之处?当然,文章中如果有拼写错误、语法错误、逻辑错误、概念错误的话也一定要告诉我!不能让我糊涂一辈子不是?:)