Unity 中的拦截功能
Dino Esposito
在上个月的专栏中,我简要介绍了 Unity 2.0 依赖关系注入容器使用的拦截机制。 在演示面向方面的编程 (AOP) 的核心概念之后,我介绍了一个具体的拦截示例,可能符合如今的很多开发人员的需要。
您是否想要扩展现有代码的行为却不想以任何方式触及源代码? 您是否希望围绕现有的代码再运行更多代码?
AOP 的目标是提供一种方法,将核心代码与其他干扰核心业务逻辑的内容隔离开。 Unity 2.0 提供基于 Microsoft .NET Framework 4 的框架来实现此目的,而且极其快速和方便。
为了使您完全理解这篇后续文章的目的,我先概要介绍上个月的内容。 您会发现,在上个月的代码中,我作了一些假设并使用了一些默认组件。 这个月我将回过头去更详细地讨论您通常会遇到的选择和选项。
Unity 中的 AOP
假设您已经部署了应用程序,以便在某个时刻执行一些与业务相关的操作。 一天,您的客户要求扩展该行为,以便执行更多工作。 您找出源代码,进行修改,然后按照编码和测试新功能所需的时间来收取咨询费用。 但如果您能顺利添加新的行为而不用触及现有的源代码,岂不是更好?
考虑一下稍有不同的情况。 首先,如果您并不是独立顾问而是全职公司员工,该怎么办呢? 收到的更改请求越多,您就得在现有项目之外花费越多的时间;更糟糕的是,您还得面临为基本代码创建新分支(并不是必需的)的风险。 因此,您会由衷地喜欢可以让您顺利添加新的行为却无需触及源代码的解决方案。
最后,假设有人报告了错误或严重的性能问题。 您需要调查并修正问题,而且您希望它不会引人注意。 在这种情况下,您同样期望能够顺利添加新的行为而不用触及源代码。
AOP 可以帮助您应对所有这些情况。
上个月,我演示了如何利用 Unity 2.0 中的拦截 API 围绕现有方法添加预处理和后处理代码,而不用触及该方法。 但这段简短的演示利用了几个假设。
首先,它利用由 Unity 反转控制 (IoC) 基础结构注册的类型,并通过 Unity 工厂层进行实例化。
其次,联接点集合只是通过接口定义的。 在 AOP 术语中,联接点集合代表目标类中的位置集合,而框架就在这些位置按需注入额外的行为。 基于接口的联接点集合表示只有该接口的成员才会通过代码注入在运行时扩展。
第三,我主要关注支持拦截的配置设置,而没有考虑能够让您在代码中配置 Unity 的 Fluent API。
在本文的其余部分,我将探讨 Fluent API 以及定义 Unity 拦截功能的其他方法。
可拦截的实例
若要为现有的类实例或新创建的类实例添加新的行为,您必须对工厂有一定的控制力。 换句话说,AOP 不是万能的,您不可能绑定通过标准的 new 运算符实例化的普通 CLR 类:
- var calculator = new Calculator();
-
AOP 框架控制实例的方式可能大有不同。 在 Unity 中,您可以求助于某些返回原始对象代理的显式调用,或者让代码完全在 IoC 框架之后运行。 为此,大多数 IoC 框架都提供 AOP 功能。 Spring.NET 和 Unity 就是两个示例。 当 AOP 和 IoC 一起使用时,就会得到顺利、轻松和有效的编码体验。
我们先从一个示例开始,其中没有使用 IoC 功能。 这里是一些基本代码,可以让现有的 Calculator 类实例变得可以拦截:
- var calculator = new Calculator();
- var calculatorProxy = Intercept.ThroughProxy<ICalculator>(calculator,
- new InterfaceInterceptor(), new[] { new LogBehavior() });
- Console.WriteLine(calculatorProxy.Sum(2, 2));
-
最后要处理一个包装了原始对象的可拦截代理。 在这种情况下,我假设 Calculator 类实现 ICalculator 接口。 若要变得可拦截,类必须实现接口或者继承自 MarshalByRefObject。 如果类派生自 MarshalByRefObject,那么拦截程序的类型必须是 TransparentProxyInterceptor:
- var calculator = new Calculator();
- var calculatorProxy = Intercept.ThroughProxy(calculator,
- new TransparentProxyInterceptor(), new[] { new LogBehavior() });
- Console.WriteLine(calculatorProxy.Sum(2, 2));
-
Intercept 类还提供 NewInstance 方法,您可以调用该方法以更直接的方式创建可拦截的对象。 以下就是使用方法:
- var calculatorProxy = Intercept.NewInstance<Calculator>(
- new VirtualMethodInterceptor(), new[] { new LogBehavior() });
-
请注意,当您使用 NewInstance 时,拦截程序组件必须稍有不同。它不能是 InterfaceInterceptor,也不能是 TransparentProxyInterceptor,而应该是 VirtualMethodInterceptor 对象。 那么 Unity 中有多少种拦截程序?
实例和类型拦截程序
拦截程序是一种 Unity 组件,该组件负责捕获对目标对象的原始调用并通过行为管道进行路由,使得每个行为都有机会在常规方法调用之前或之后运行。 拦截的类型有两种:实例拦截和类型拦截。
实例拦截程序创建代理以筛选针对所拦截实例的传入调用。 类型拦截程序生成新的类(这个类派生自要拦截的类型),并处理该派生类型的实例。 不用说,原始类型和派生类型的区别就在于用来筛选传入调用的逻辑。
对于实例拦截,应用程序代码首先使用传统的工厂(或 new 运算符)创建目标对象,然后强制通过 Unity 提供的代理与其交互。
对于类型拦截,应用程序通过 API 或 Unity 创建目标对象,然后处理该实例。 (您无法使用 new 运算符直接创建对象并获得类型拦截。)但是目标对象不是原始类型。 实际的类型由 Unity 实时派生,并且会加入拦截逻辑(请参见图 1)。
图 1 实例拦截程序和类型拦截程序
InterfaceInterceptor 和 TransparentProxyInterceptor 是两个 Unity 拦截程序,属于实例拦截程序类别。 VirtualMethodInterceptor 属于类型拦截程序类别。
InterfaceInterceptor 可以拦截目标对象上的一个接口的公共实例方法。 该拦截程序可以应用到新的和现有的实例。
TransparentProxyInterceptor 可以拦截多个接口和按引用封送的对象上的公共实例方法。 这是最慢的拦截方式,但可以拦截的方法最多。 该拦截程序可以应用到新的和现有的实例。
VirtualMethodInterceptor 可以拦截公共和受保护的虚拟方法。 该拦截程序只能应用到新的实例。
应该注意的是,实例拦截可以应用到任意公共的实例方法,但不能应用到构造函数。 这在将拦截应用到现有实例时相当明显, 而将拦截应用到新创建的实例时则不那么明显。 实例拦截的实现方式是构造函数在应用程序代码取回要处理的对象时已经执行。 结果,任何可拦截操作都必须在创建实例之后。
类型拦截使用动态代码生成来返回从原始类型继承的对象。 在这种情况下,任何公共和受保护的虚拟方法都被重写,以便支持拦截。 请考虑使用以下代码:
- var calculatorProxy = Intercept.NewInstance<Calculator>(
- new VirtualMethodInterceptor(), new[] { new LogBehavior() });
-
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 配置:
- // Configure the IoC container
- var container = UnityStarter.Initialize();
-
- // Start the application
- var calculator = container.Resolve<ICalculator>();
- var result = calculator.Sum(2, 2);
-
启动容器所需的代码可以隔离在不同的类中,并在应用程序启动时调用。 启动代码将指导容器如何围绕应用程序解析类型以及如何处理拦截。 调用 Resolve 方法可以为您屏蔽拦截的所有细节。 图 3 显示了启动代码可能的实现方式。
图 3 启动 Unity
- public class UnityStarter {
- public static UnityContainer Initialize() {
- var container = new UnityContainer();
-
- // Enable interception in the current container
- container.AddNewExtension<Interception>();
-
- // Register ICalculator with the container and map it to
- // an actual type.
- In addition, specify interception details.
- container.RegisterType<ICalculator, Calculator>(
- new Interceptor<VirtualMethodInterceptor>(),
- new InterceptionBehavior<LogBehavior>());
-
- return container;
- }
- }
-
比较有利的一点是这段代码可以移动到独立的程序集中,动态加载或更改。 更重要的是,您可以在一个位置配置 Unity。 如果您坚持使用 Intercept 类(其行为就像智能工厂,每次使用时都需要做准备),就无法做到这一点。 因此,如果您的应用程序需要 AOP,请务必通过 IoC 容器获得。 如果将配置的详细信息移到 app.config 文件(如果是 Web 应用程序则是 web.config)中,就可以用更灵活的方式实现相同的解决方案。 在这种情况下,启动代码包含以下两行:
- var container = new UnityContainer();
- container.LoadConfiguration();
-
图 4 显示了配置文件中必须包含的脚本。 在这里,我为 ICalculator 类型注册了两种行为。 这表示对接口公共成员的所有调用都将由 LogBehavior 和 BinaryBehavior 进行预处理和后处理。
图 4 通过配置添加拦截细节
- <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
- <assembly name="SimplestWithConfigIoC"/>
- <namespace name="SimplestWithConfigIoC.Calc"/>
- <namespace name="SimplestWithConfigIoC.Behaviors"/>
-
- <sectionExtension
- type="Microsoft.Practices.Unity.
- InterceptionExtension.Configuration.
- InterceptionConfigurationExtension,
- Microsoft.Practices.Unity.Interception.Configuration" />
-
- <container>
- <extension type="Interception" />
-
- <register type="ICalculator" mapTo="Calculator">
- <interceptor type="InterfaceInterceptor"/>
- <interceptionBehavior type="LogBehavior"/>
- <interceptionBehavior type="BinaryBehavior"/>
- </register>
-
- <register type="LogBehavior">
- </register>
-
- <register type="BinaryBehavior">
- </register>
-
- </container>
- </unity>
-
请注意,由于 LogBehavior 和 BinaryBehavior 是具体的类型,因此您实际上根本不需要注册它们。 Unity 的默认设置会自动处理它们。
行为
在 Unity 中,行为是真正实现横切关注点的对象。 作为实现 IInterceptionBehavior 接口的类,行为将覆盖被拦截方法的执行循环,并且可以修改方法参数或返回值。 行为甚至可以完全阻止方法被调用,或者多次调用方法。
一个行为由三个方法组成。 图 5 显示了一个拦截方法 Sum 并将返回值修改为二进制字符串的示例行为。 方法 WillExecute 只是一种优化代理的方式。 如果它返回 False,行为就不会执行。
图 5 行为示例
- public class BinaryBehavior : IInterceptionBehavior {
- public IEnumerable<Type> GetRequiredInterfaces() {
- return Type.EmptyTypes;
- }
-
- public bool WillExecute {
- get { return true; }
- }
-
- public IMethodReturn Invoke(
- IMethodInvocation input,
- GetNextInterceptionBehaviorDelegate getNext) {
-
- // Perform the operation
- var methodReturn = getNext().Invoke(input, getNext);
-
- // Grab the output
- var result = methodReturn.ReturnValue;
-
- // Transform
- var binaryString = ((Int32)result).ToBinaryString();
-
- // For example, write it out
- Console.WriteLine("Rendering {0} as binary = {1}",
- result, binaryString);
-
- return methodReturn;
- }
- }
-
这其实有点微妙。 Invoke 总是被调用,因此即使返回 False,您的行为实际上也会执行。 但是在创建代理或派生类型时,如果为该类型注册的所有行为都将 WillExecute 设置为 False,那么也就不会创建代理本身,您将再次处理原始对象。 这实际上是在优化代理创建。
GetRequiredInterfaces 方法允许行为向目标对象添加新接口,从此方法返回的接口将添加到代理中。 因此,行为的核心就是 Invoke 方法。 该参数输入让您可以访问目标对象上正在调用的方法。 参数 getNext 是一个委托,用于移动到管道中下一个行为,并且最终执行目标上的方法。
Invoke 方法确定调用目标对象上的公共方法时所用的实际逻辑。 请注意,目标对象上所有被拦截的方法都将按照 Invoke 中表达的逻辑执行。
如果要使用更特殊的匹配规则,该怎么办呢? 使用我在本文中介绍的普通拦截,您能做的就是运行一组 IF 语句,来找出被调用的是哪个方法,如下所示:
- if(input.MethodBase.Name == "Sum") {
- ...
- }
-
下个月我将继续这个话题,探讨以更有效的方式应用拦截,为被拦截的方法定义匹配规则。