高性能 AOP 框架 AspectInjector 教程
AspectInjector—— 一个免费开源框架,能够在编译时(compile-time,下同)期间,运用切面(Aspect,下同)实现对应用程序静态注入,并且具备简单却不失灵活的接口。 与更常用的运行时(run-time)代理生成框架(例如: Unity
和 Castle.Core.AsyncInterceptor
)相比,编译时 AOP 显然提供了更好的性能,这在某些情况下非常重要。
AspectInjector 自其最稳定的测试版发布以来,已经过去了五年。在此期间,开发团队进行了多次改进及重大变化,API 最终稳定了下来。
现在,我从一个简单的例子开始,说明 AspectInjector 可以轻松做到的事情。
假设有这样的场景:需要对所有方法进行简单的跟踪——只需捕获方法的开始和结束。 为了实现此目的,需要定义一个实现所需行为的切面:
public sealed
要将切面应用于需要它的任何类,只需使用刚创建的特性(Attribute,下同)标记目标类:
因此,对 TraceStart
和 TraceFinish
的调用将注入到 SampleService
的所有方法中。
现在,让我们更详细地研究一下用于定义切面的特性。为便于下文理解,上述示例代码中,切面类和特性类属同一个类,即 TraceAspectAttribute
。
Aspect
注解于切面类上。它的必需参数( mandatory parameter)用来指定应如何创建切面实例——整个应用程序的单个实例(Scope.Global
),或为每个目标对象的新实例(Scope.PerInstance
)创建切面实例。此特性还有一个可选参数 Factory,用于指定用于实例化切面的工厂类,其用法示例可以在本文后面找到。Injection
将指定的切面类,注解在应用于目标(SampleService
)的特性上。 仅有一个必需参数,用来接受要应用的切面类类型(typeof(TraceAspectAttribute)
),其余可选的优先级参数允许控制切面注入的顺序:其值越高,将执行此切面的越早方法。Advice
注解于切面类的方法,该方法将作为切面的一部分注入到目标。它有一个必需参数 Kind,用于指定带注解的方法将如何应用于目标:Kind.Before
—— 指示在调用目标之前注入此(Advice
注解的)方法;Kind.After
—— 指示在从目标返回后立即注入此(Advice
注解的)方法;Kind.Around
—— 允许通过指定的(Advice
注解的)方法包装原始目标。为了使它成为可能,(Advice
注解的)方法可能有两个参数,对应于目标的委托和目标的调用参数。此特性的非必需参数Targets
也非常重要——它允许控制应将该切面应用于哪种类成员(getter、setter、常规方法、构造函数、静态成员等)。 可以在Target
枚举中找到可能值的完整列表,列表中的每个值都是自描述的。 Targets 参数的默认值为Target.Any
,这意味着将为每个类成员注入该Advice
。Argument
用于Advice
注解的方法参数,指定应通过此参数传递有关目标的哪些信息。 该特性的 “source” 参数的每个值都需要一个特定的Advice
参数类型:Source.Instance
(object)——此Advice
已注入的对象实例;Source.Type
(Type)——目标类型;Source.Method
(MethodBase) —— 目标的元数据;Source.Target
(Func<object[], object>) – 目标的委托;Source.Name
(string) —— 目标的名称;Source.Arguments
(object[]) —— 原始调用参数;Source.ReturnValue
(object) —— 原始调用返回的值;Source.ReturnType
(Type) —— 目标的返回类型;Source.Injections
(Attribute[]) —— 添加此Advice
的注入特性列表。
上面的日志记录示例可能会引发有关如何传递记录器实例的问题,如果想要使用日志记录框架而不仅仅是写入控制台。有两种方法:
- 通过静态属性访问记录器或记录器工厂,并直接在切面代码中使用它。显然,它并不理想,因为它不允许使用依赖注入技术。
- 有一个工厂类,该工厂类处理内部的所有依赖关系问题,并具有一个静态 GetInstance(Type aspectType) 方法,用于创建指定类型的切面。
让我们看一下与上面的日志记录示例相关的第二种方法的可能实现。 除了添加切面工厂之外,我们还将增强日志记录切面,以封装测量调用时间的方式。
public sealed
public sealed
public
在这里,你可以看到为该特性创建了一个单独的类,因为现在切面( aspect)类需要传入一些外部依赖项,但该特性的构造函数不应有任何参数。
在切面的定义中,指定的工厂类必须具有静态方法 GetInstance,如上面示例中的签名:它接受类型并返回对象。对象创建的所有细节都可隐藏在方法内部,可以使用 DI 容器来实现目标。与“传统“ DI 容器使用相比,这种方法的唯一缺点是必须具有这种静态的“服务定位器“。不幸的是,编译时 AOP 没有其他方法可以做到这一点——工厂方法必须在编译时可用。
最后,但并非最不重要的一点是 —— Advice
的 Kind.Around
。它允许将调用完全包装在新方法中,并在原始调用之前和之后引入任何其他逻辑。在此示例中,秒表在基础调用之前创建,然后用于计算其持续时间。如果没有 Advice
的 Kind.Around
,将某些状态从“调用前“上下文可靠地传递到“调用后“将相当具有挑战性。请注意,任何 Advice
的 Kind.Around
方法必须至少有两个参数——目标方法委托 (Source.Target) 和原始调用参数 (Source.Arguments)。
让我们还看一下 Aspect Injector 的另一个重要特性——接口实现注入。 下面的示例演示如何使任意类实现 INotifyPropertyChanged 接口,就像每个公共属性在每次调用其 setter 时调用 PropertyChaged 事件处理程序一样。
public
internal
如你所见,要创建注入接口实现的切面,只需在切面中实现所需的接口,并使用 Mixin 特性对其进行注解,指定要注入目标类的接口。上面用 Notify 特性注解的每个类都将在编译期间获得 INotifyPropertyChanged 及其实现。
总体而言,当应用程序框架不提供任何注入操作处理管道的方法时,此库在许多情况下可以提供帮助。并且由于它隐式实现编译时注入,因此它提供了比基于动态代理的 AOP 框架更好的性能。
本文编译自:Aspect-oriented programming in .NET with AspectInjector