对EnterLib有所了解的人应该知道,其中有一个名叫Policy Injection的AOP框架;而整个EnterLib完全建立在另一个叫作Unity的底层框架之上,我们可以将Unity看成是一个IoC的框架。对于一个企业应用来说说,AOP和IoC是我们进行逻辑分离和降低耦合度最主要的方式,而将两者结合起来具有重要的现实意义。
一、基于IoC+AOP的编程到底将IoC和AOP进行整合后,会对编程但来怎样的影响,我写了一个简单的例子(你可以从这里下载该实例)。假设我现在有两个模块,分别称为Foo和Bar,通过如下同名的类来表示。Foo和Bar具有各自的接口,分别为IFoo和IBar。简单起见,我在两个接口中定义了相同的方法:DoSomething。在Foo中,具有一个类型为IBar的只读属性,而DoSomething的实现就是通过调用该属性的同名方法实现。
1: public interface IFoo
2: {
3: void DoSomething();
4: }
5:
6: public interface IBar
7: {
8: void DoSomething();
9: }
10: [FooCallHandler]
11: public class Foo : IFoo
12: {
13: public IBar Bar { get; private set; }
14: public Foo(IBar bar)
15: { this.Bar = bar; }
16: public void DoSomething()
17: {
18: this.Bar.DoSomething();
19: }
20: }
21: [BarCallHandler]
22: public class Bar : IBar
23: {
24: public void DoSomething()
25: {
26: Console.WriteLine("Do something...");
27: }
28: }
在类型Foo和Bar上分别应用了两个自定义特性:FooCallHandlerAttribute和BarCallHandlerAttribute,熟悉PIAB的读者对此应该不感到陌生——CallHandler承载着被分离出来的横切关注点(Crosscutting Concern)的实现。由于在这里仅仅是一个简单的演示,我定义了两个最简单的CallHandler:FooCallHandler和BarCallHandler。下面是这个两个CallHandler的对应的HandlerAttribute的定义。
1: public class FooCallHandler : ICallHandler
2: {
3: public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
4: {
5: Console.WriteLine("Foo: Preoperation is executed.");
6: var methodReturn = getNext()(input, getNext);
7: Console.WriteLine("Foo: Postoperation is executed.");
8: return methodReturn;
9: }
10: public int Order { get; set; }
11: }
12: public class BarCallHandler : ICallHandler
13: {
14: public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
15: {
16: Console.WriteLine("Bar: Preoperation is executed.");
17: var methodReturn = getNext()(input, getNext);
18: Console.WriteLine("Bar: Postoperation is executed.");
19: return methodReturn;
20: }
21: public int Order { get; set; }
22: }
23: public class FooCallHandlerAttribute : HandlerAttribute
24: {
25: public override ICallHandler CreateHandler(IUnityContainer container)
26: {
27: return new FooCallHandler { Order = this.Order };
28: }
29: }
30: public class BarCallHandlerAttribute : HandlerAttribute
31: {
32: public override ICallHandler CreateHandler(IUnityContainer container)
33: {
34: return new BarCallHandler { Order = this.Order };
35: }
36: }
两个CallHandler具有相似的实现:在目标方法执行前后打印相应的文字,代表在对方法调用进行拦截后,需要在方法执行前后实现的横切关注点。目前为止,提供具体功能的组建定义完成,我们来编写消费它们的客户程序:
1: static void Main(string[] args)
2: {
3: var container = GetContainer();
4: var foo = container.Resolve<IFoo>();
5: foo.DoSomething();
6: }
从上面我们可以看出,我们最终的代码仅仅只要三行。虽然简单,我们不妨来也做一下分析:首先,客户端对组件Foo的调用是基于接口(IFoo)而不是基于具体类型Foo,这样能够尽可能地降低对组件Foo的依赖;其次,Foo依赖于Bar,而这种依赖也是基于接口的,这在Foo的定义了看得出来;第三,虽然Foo依赖于Bar,但是这种依赖是和Foo的具体实现相关,和客户程序无关,所以客户程序未曾出现Bar和IBar的影子。
运行上面的程序,你会得到如下的输出。从中我们可以看出:不但具体的业务功能(即定义在Bar的DoSomething方法中的逻辑)能够正常执行,通过自定义特性的方式应用到两个组件上动态注入的横切关注点也得到正确地执行。
1: Foo: Preoperation is executed.
2: Bar: Preoperation is executed.
3: Do something...
4: Bar: Postoperation is executed.
5: Foo: Postoperation is executed.
那么,是什么导致了程序的完全按照我们希望的方式执行的呢?由于客户端逻辑只有三句代码,你肯定会猜的到:所有的一切都是由一开始创建的container对象完成的。如何你了解Unity的话,应该可以猜出这是一个UnityContainer。通过接口和类型的匹配关系的注册,UnityContainer知道如何根据接口找到相应的实现类型(IFoo-〉Foo,IBar-〉Bar),这不难理解,这也不是本篇文章介绍的重点。我们关注点是:UnityContainer是如何让通过自定义特性方式应用在Foo和Bar上的两个CallHandler得到执行的?
二、CallHandler是如何被执行的同所有的AOP实现一样,PIAB也是采用方法拦截(Method Interception)机制。具体来说,PIAB又具有两个不同的方式:实例拦截(Instance Interception)和类型拦截(Type Interception)。而前者具有两种不同的实现:TransparentProxy/RealProxy的方式和Reflection Emit的方式。三种不同拦截方式的具体实现,不是本文的重点,对此有兴趣的朋友可以参阅PIAB官方文档。
总的来说,能够在方法执行过程中被拦截的对象需要通过PIAB的方式进行创建,实际上PIAB是对目标对象进行相应的封装,进而得到一个代理对象。由于方法调用请求是从代理对象发出,PIAB在将该请求发送给最终的目标对象之前将其拦截下来,从而使相应的CallHandler得以执行。那么,如果我们能够让UnityContainer在对象创建过程完成这一道工序,那么最终被创建出来的对象就具有了被拦截的能力。
如何将PIAB对实例的封装操作注入到UnityContainer怎个对象创建流程中呢?这需要借助于UnityContainer提供的扩展机制。虽然Unity仅仅是一个轻量级的IoC框架,但是内部的实现其实是挺复杂的。个人曾经不止一次地分析过Unity的源代码,但是没过多久就忘得七七八八。不过,万变不离其宗,UnityContainer最根本的就是其BuilderStrategy管道(可以参阅我的文章《你知道Unity IoC Container是如何创建对象的吗?》)。
我们的解决方案就是将PIAB对实例的封装写在相应的BuilderStrategy种,然后通过UnityContainerExtension注册到某个UnityContainer中。为此我创建了如下一个名称为InterceptionStrategy的BuilderStrategy。
1: public class InterceptionStrategy : BuilderStrategy
2: {
3: public static IUnityContainer UnityContainer { get; private set; }
4: public static InjectionMember InterceptionMember { get; private set; }
5:
6: static InterceptionStrategy()
7: {
8: IUnityContainer container = new UnityContainer();
9: UnityContainerConfigurator configurator = new UnityContainerConfigurator(container);
10: EnterpriseLibraryContainer.ConfigureContainer(configurator, ConfigurationSourceFactory.Create());
11: UnityContainer = container;
12: InterceptionMember = new InstanceInterceptionPolicySettingInjectionMember(new TransparentProxyInterceptor());
13: }
14:
15: public override void PostBuildUp(IBuilderContext context)
16: {
17: if (null == context.Existing ||
18: context.Existing.GetType().FullName.StartsWith("Microsoft.Practices") ||
19: context.Existing is IInterceptingProxy)
20: {
21: return;
22: }
23: context.Existing = UnityContainer.Configure<TransientPolicyBuildUpExtension>().BuildUp
24: (context.OriginalBuildKey.Type, context.Existing, null, InterceptionMember);
25: }
26: }
如果你对Unity的内部机制有一定了解,理解上面的实现应该不成问题,但是你对此一无所知,我讲解得再详细你可能也弄不清楚。所以,对于上述的逻辑实现,在这里就不多作介绍了。
自定义的BuilderStrategy一般需要通过相应的UnityContainerExtension完成注册。下面是我们创建的UnityContainerExtension:InterceptionExtension。我们将UnityContainerExtension对象添加到UnityContainer的BuilderStrategy管道之中,相应的BuilderStage设置为PreCreation。
1: public class InterceptionExtension: UnityContainerExtension
2: {
3: protected override void Initialize()
4: {
5: Context.Strategies.AddNew<InterceptionStrategy>(UnityBuildStage.PreCreation);
6: }
7: }
三、GetContainer()的实现…
最初例子得以正常运行的魔力来自于通过静态方法GetContainer创建的UnityContainer对象,我们现在来看看该方法的实现。首先我们创建一个UnityContainer对象,然后对其进行初始化配置,最后将上面创建的InterceptionExtension扩展添加到该UnityContainer中。接口和实现类型的注册被随后执行,不过在真正的开发中,我们习惯通过配置文件进行注册。这就是整个实现,没有复杂的逻辑,却能带来很大的用处。
1: static IUnityContainer GetContainer()
2: {
3: IUnityContainer container = new UnityContainer();
4: UnityContainerConfigurator configurator = new UnityContainerConfigurator(container);
5: EnterpriseLibraryContainer.ConfigureContainer(configurator, ConfigurationSourceFactory.Create());
6: container.AddNewExtension<InterceptionExtension>();
7: container.RegisterType<IFoo, Foo>();
8: container.RegisterType<IBar, Bar>();
9: return container;
10: }
四、申明
文中提供解决方案只是为你提供一种思路,相关的逻辑实现基本来自于脑子中的灵光一现,并没有进行深入的评估和性能测试。如果你希望在你自己的项目中使用,最好在此基础上进行深入的思考,相信会发现其中存在的不足。此外,不知道读者有没有注意,上面的实现方式仅仅提供一种拦截方式,即基于TransparentProxy的方式,有兴趣的读者可以再作一些扩展,实现对拦截方式的定制。
本文所介绍的实现实际上是采用EnterLib PIAB的机制。由于PIAB本身的问题,上面的实现只能支持构造器注入,对属性注入和方法注入不提供支持,详情请参考《EnterLib PIAB又一个BUG?[续]——这是一个致命的BUG》