Unity 中的拦截功能(转)

(转自MSDN杂志:http://msdn.microsoft.com/zh-cn/magazine/gg535676.aspx)javascript

上个月的专栏中,我简要介绍了 Unity 2.0 依赖关系注入容器使用的拦截机制。 在演示面向方面的编程 (AOP) 的核心概念以后,我介绍了一个具体的拦截示例,可能符合现在的不少开发人员的须要。java

您是否想要扩展示有代码的行为却不想以任何方式触及源代码? 您是否但愿围绕现有的代码再运行更多代码?web

AOP 的目标是提供一种方法,将核心代码与其余干扰核心业务逻辑的内容隔离开。 Unity 2.0 提供基于 Microsoft .NET Framework 4 的框架来实现此目的,并且极其快速和方便。编程

为了使您彻底理解这篇后续文章的目的,我先概要介绍上个月的内容。 您会发现,在上个月的代码中,我做了一些假设并使用了一些默认组件。 这个月我将回过头去更详细地讨论您一般会遇到的选择和选项。app

Unity 中的 AOP

假设您已经部署了应用程序,以便在某个时刻执行一些与业务相关的操做。 一天,您的客户要求扩展该行为,以便执行更多工做。 您找出源代码,进行修改,而后按照编码和测试新功能所需的时间来收取咨询费用。 但若是您能顺利添加新的行为而不用触及现有的源代码,岂不是更好?框架

考虑一下稍有不一样的状况。 首先,若是您并非独立顾问而是全职公司员工,该怎么办呢? 收到的更改请求越多,您就得在现有项目以外花费越多的时间;更糟糕的是,您还得面临为基本代码建立新分支(并非必需的)的风险。 所以,您会由衷地喜欢可让您顺利添加新的行为却无需触及源代码的解决方案。函数

最后,假设有人报告了错误或严重的性能问题。 您须要调查并修正问题,并且您但愿它不会引人注意。 在这种状况下,您一样指望可以顺利添加新的行为而不用触及源代码。性能

AOP 能够帮助您应对全部这些状况。测试

上个月,我演示了如何利用 Unity 2.0 中的拦截 API 围绕现有方法添加预处理和后处理代码,而不用触及该方法。 但这段简短的演示利用了几个假设。优化

首先,它利用由 Unity 反转控制 (IoC) 基础结构注册的类型,并经过 Unity 工厂层进行实例化。

其次,联接点集合只是经过接口定义的。 在 AOP 术语中,联接点集合表明目标类中的位置集合,而框架就在这些位置按需注入额外的行为。 基于接口的联接点集合表示只有该接口的成员才会经过代码注入在运行时扩展。

第三,我主要关注支持拦截的配置设置,而没有考虑可以让您在代码中配置 Unity 的 Fluent API。

在本文的其他部分,我将探讨 Fluent API 以及定义 Unity 拦截功能的其余方法。

可拦截的实例

若要为现有的类实例或新建立的类实例添加新的行为,您必须对工厂有必定的控制力。 换句话说,AOP 不是万能的,您不可能绑定经过标准的 new 运算符实例化的普通 CLR 类:

  1.  
  2.           var calculator = new Calculator();
  3.         

AOP 框架控制实例的方式可能大有不一样。 在 Unity 中,您能够求助于某些返回原始对象代理的显式调用,或者让代码彻底在 IoC 框架以后运行。 为此,大多数 IoC 框架都提供 AOP 功能。 Spring.NET 和 Unity 就是两个示例。 当 AOP 和 IoC 一块儿使用时,就会获得顺利、轻松和有效的编码体验。

咱们先从一个示例开始,其中没有使用 IoC 功能。 这里是一些基本代码,可让现有的 Calculator 类实例变得能够拦截:

  1.  
  2.           var calculator = new Calculator();
  3. var calculatorProxy = Intercept.ThroughProxy<ICalculator>(calculator,
  4.   new InterfaceInterceptor(), new[] { new LogBehavior() });
  5. Console.WriteLine(calculatorProxy.Sum(22));
  6.         

最后要处理一个包装了原始对象的可拦截代理。 在这种状况下,我假设 Calculator 类实现 ICalculator 接口。 若要变得可拦截,类必须实现接口或者继承自 MarshalByRefObject。 若是类派生自 MarshalByRefObject,那么拦截程序的类型必须是 TransparentProxyInterceptor:

  1.  
  2.           var calculator = new Calculator();
  3. var calculatorProxy = Intercept.ThroughProxy(calculator,
  4.   new TransparentProxyInterceptor(), new[] { new LogBehavior() });
  5. Console.WriteLine(calculatorProxy.Sum(22));
  6.         

Intercept 类还提供 NewInstance 方法,您能够调用该方法以更直接的方式建立可拦截的对象。 如下就是使用方法:

  1.  
  2.           var calculatorProxy = Intercept.NewInstance<Calculator>(
  3.   new VirtualMethodInterceptor(), new[] { new LogBehavior() });
  4.         

请注意,当您使用 NewInstance 时,拦截程序组件必须稍有不一样。它不能是 InterfaceInterceptor,也不能是 TransparentProxyInterceptor,而应该是 VirtualMethodInterceptor 对象。 那么 Unity 中有多少种拦截程序?

实例和类型拦截程序

拦截程序是一种 Unity 组件,该组件负责捕获对目标对象的原始调用并经过行为管道进行路由,使得每一个行为都有机会在常规方法调用以前或以后运行。 拦截的类型有两种:实例拦截和类型拦截。

实例拦截程序建立代理以筛选针对所拦截实例的传入调用。 类型拦截程序生成新的类(这个类派生自要拦截的类型),并处理该派生类型的实例。 不用说,原始类型和派生类型的区别就在于用来筛选传入调用的逻辑。

对于实例拦截,应用程序代码首先使用传统的工厂(或 new 运算符)建立目标对象,而后强制经过 Unity 提供的代理与其交互。

对于类型拦截,应用程序经过 API 或 Unity 建立目标对象,而后处理该实例。 (您没法使用 new 运算符直接建立对象并得到类型拦截。)可是目标对象不是原始类型。 实际的类型由 Unity 实时派生,而且会加入拦截逻辑(请参见图 1)。

图 1 实例拦截程序和类型拦截程序

InterfaceInterceptor 和 TransparentProxyInterceptor 是两个 Unity 拦截程序,属于实例拦截程序类别。VirtualMethodInterceptor 属于类型拦截程序类别。

InterfaceInterceptor 能够拦截目标对象上的一个接口的公共实例方法。 该拦截程序能够应用到新的和现有的实例。

TransparentProxyInterceptor 能够拦截多个接口和按引用封送的对象上的公共实例方法。 这是最慢的拦截方式,但能够拦截的方法最多。 该拦截程序能够应用到新的和现有的实例。

VirtualMethodInterceptor 能够拦截公共和受保护的虚拟方法。 该拦截程序只能应用到新的实例。

应该注意的是,实例拦截能够应用到任意公共的实例方法,但不能应用到构造函数。 这在将拦截应用到现有实例时至关明显, 而将拦截应用到新建立的实例时则不那么明显。 实例拦截的实现方式是构造函数在应用程序代码取回要处理的对象时已经执行。 结果,任何可拦截操做都必须在建立实例以后。

类型拦截使用动态代码生成来返回从原始类型继承的对象。 在这种状况下,任何公共和受保护的虚拟方法都被重写,以便支持拦截。 请考虑使用如下代码:

  1.  
  2.           var calculatorProxy = Intercept.NewInstance<Calculator>(
  3.   new VirtualMethodInterceptor(), new[] { new LogBehavior() });
  4.         

Calculator 类以下所示:

public class Calculator { public virtual Int32 Sum(Int32 x, Int32 y) { return x + y; } }

图 2 显示了对 calculatorProxy 变量进行动态检查后获得的类型的实际名称。

图 2 类型拦截以后的实际类型

另外还要注意实例拦截和类型拦截之间存在的其余显著区别,例如按照调用的对象拦截调用。 使用类型拦截时,若是一个方法调用同一对象上的另外一个方法,那么该自我调用就能够被拦截,由于拦截逻辑在同一个对象中。 可是,对于实例拦截,则只有当调用经过代理进行时,才能发生拦截。 固然,自我调用不须要通过代理,所以不会发生拦截。

使用 IoC 容器

在上个月的示例中,我使用了 Unity 库的 IoC 容器来完成对象的建立。 IoC 容器是围绕对象建立的一个额外层,能够增长应用程序的灵活性。 若是您将 IoC 框架与更多 AOP 功能相结合,就更是如此。 此外(我是这样认为的),若是您将 IoC 容器与离线配置结合使用,代码的灵活程度将超乎想象。 可是,下面这个示例将使用 Unity 的容器以及基于代码的 Fluent 配置:

  1.  
  2.           // Configure the IoC container
  3. var container = UnityStarter.Initialize();
  4.  
  5. // Start the application
  6. var calculator = container.Resolve<ICalculator>();
  7. var result = calculator.Sum(22);
  8.         

启动容器所需的代码能够隔离在不一样的类中,并在应用程序启动时调用。 启动代码将指导容器如何围绕应用程序解析类型以及如何处理拦截。 调用 Resolve 方法能够为您屏蔽拦截的全部细节。 图 3 显示了启动代码可能的实现方式。

图 3 启动 Unity

  1.  
  2.           public class UnityStarter {
  3.   public static UnityContainer Initialize() {
  4.     var container = new UnityContainer();
  5.  
  6.     // Enable interception in the current container 
  7.     container.AddNewExtension<Interception>();
  8.  
  9.     // Register ICalculator with the container and map it to 
  10.     // an actual type.
  11.           In addition, specify interception details.
  12.           container.RegisterType<ICalculator, Calculator>(
  13.       new Interceptor<VirtualMethodInterceptor>(),
  14.       new InterceptionBehavior<LogBehavior>());
  15.  
  16.     return container;
  17.   }
  18. }
  19.         

比较有利的一点是这段代码能够移动到独立的程序集中,动态加载或更改。 更重要的是,您能够在一个位置配置 Unity。 若是您坚持使用 Intercept 类(其行为就像智能工厂,每次使用时都须要作准备),就没法作到这一点。 所以,若是您的应用程序须要 AOP,请务必经过 IoC 容器得到。 若是将配置的详细信息移到 app.config 文件(若是是 Web 应用程序则是 web.config)中,就能够用更灵活的方式实现相同的解决方案。 在这种状况下,启动代码包含如下两行:

  1.  
  2.           var container = new UnityContainer();
  3. container.LoadConfiguration();
  4.         

图 4 显示了配置文件中必须包含的脚本。 在这里,我为 ICalculator 类型注册了两种行为。 这表示对接口公共成员的全部调用都将由 LogBehavior 和 BinaryBehavior 进行预处理和后处理。

图 4 经过配置添加拦截细节

  1.  
  2.           <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
  3.   <assembly name="SimplestWithConfigIoC"/>
  4.   <namespace name="SimplestWithConfigIoC.Calc"/>
  5.   <namespace name="SimplestWithConfigIoC.Behaviors"/>
  6.  
  7.   <sectionExtension 
  8.     type="Microsoft.Practices.Unity.
  9.           InterceptionExtension.Configuration.
  10.           InterceptionConfigurationExtension,     
  11.       Microsoft.Practices.Unity.Interception.Configuration" />
  12.  
  13.   <container>
  14.     <extension type="Interception" />
  15.  
  16.     <register type="ICalculator" mapTo="Calculator">
  17.       <interceptor type="InterfaceInterceptor"/>
  18.       <interceptionBehavior type="LogBehavior"/>
  19.       <interceptionBehavior type="BinaryBehavior"/>
  20.     </register>
  21.  
  22.     <register type="LogBehavior">
  23.     </register>
  24.  
  25.     <register type="BinaryBehavior">
  26.     </register>
  27.  
  28.   </container>
  29. </unity>
  30.         

请注意,因为 LogBehavior 和 BinaryBehavior 是具体的类型,所以您实际上根本不须要注册它们。 Unity 的默认设置会自动处理它们。

行为

在 Unity 中,行为是真正实现横切关注点的对象。 做为实现 IInterceptionBehavior 接口的类,行为将覆盖被拦截方法的执行循环,而且能够修改方法参数或返回值。 行为甚至能够彻底阻止方法被调用,或者屡次调用方法。

一个行为由三个方法组成。 图 5 显示了一个拦截方法 Sum 并将返回值修改成二进制字符串的示例行为。 方法 WillExecute 只是一种优化代理的方式。 若是它返回 False,行为就不会执行。

图 5 行为示例

  1.  
  2.           public class BinaryBehavior : IInterceptionBehavior {
  3.   public IEnumerable<Type> GetRequiredInterfaces() {
  4.     return Type.EmptyTypes;
  5.   }
  6.  
  7.   public bool WillExecute {
  8.     get { return true; }
  9.   }
  10.  
  11.   public IMethodReturn Invoke(
  12.     IMethodInvocation input, 
  13.     GetNextInterceptionBehaviorDelegate getNext) {
  14.  
  15.     // Perform the operation
  16.     var methodReturn = getNext().Invoke(input, getNext);
  17.  
  18.     // Grab the output
  19.     var result = methodReturn.ReturnValue;
  20.  
  21.     // Transform
  22.     var binaryString = ((Int32)result).ToBinaryString();
  23.  
  24.     // For example, write it out
  25.     Console.WriteLine("Rendering {0} as binary = {1}"
  26.       result, binaryString);
  27.  
  28.     return methodReturn;
  29.   }
  30. }
  31.         

这其实有点微妙。 Invoke 老是被调用,所以即便返回 False,您的行为实际上也会执行。 可是在建立代理或派生类型时,若是为该类型注册的全部行为都将 WillExecute 设置为 False,那么也就不会建立代理自己,您将再次处理原始对象。 这其实是在优化代理建立。

GetRequiredInterfaces 方法容许行为向目标对象添加新接口,今后方法返回的接口将添加到代理中。 所以,行为的核心就是 Invoke 方法。 该参数输入让您能够访问目标对象上正在调用的方法。 参数 getNext 是一个委托,用于移动到管道中下一个行为,而且最终执行目标上的方法。

Invoke 方法肯定调用目标对象上的公共方法时所用的实际逻辑。 请注意,目标对象上全部被拦截的方法都将按照 Invoke 中表达的逻辑执行。

若是要使用更特殊的匹配规则,该怎么办呢? 使用我在本文中介绍的普通拦截,您能作的就是运行一组 IF 语句,来找出被调用的是哪一个方法,以下所示:

  1.  
  2.           if(input.MethodBase.Name == "Sum") {
  3.   ...
  4.           }
  5.         

下个月我将继续这个话题,探讨以更有效的方式应用拦截,为被拦截的方法定义匹配规则。

相关文章
相关标签/搜索