又一个面向 .NET 的面向方面编程框架
介绍 .NET 的新 AOP 框架 - AspectInjector
引言
在某些情况下,面向方面编程非常有帮助,例如日志记录、审计、安全性等。尽管 .NET 平台没有现成的相关工具,但有一些第三方解决方案提供了这种功能:PostSharp、Unity、Spring .NET、Castle Windsor、Aspect .NET... 这还不是全部。然而,在为特定任务选择 AOP 框架时,值得思考一下实现横切功能注入的机制及其优缺点。总的来说,可以确定两种方法
- 编译时注入 (PostSharp)
- 运行时代理类生成 (Unity, Spring .NET, Castle Windsor)
编译时注入是最有利的选择,因为它在程序执行期间不需要额外的计算开销,这对于移动设备尤其重要。虽然普通的代理类生成具有更简单的实现,但除了计算开销之外,它还有一些限制——方法和属性必须属于一个接口或为虚拟,才能通过代理类进行拦截。
PostSharp 为面向方面编程提供了强大的功能,但它是一个商业产品,因此可能不适用于某些项目。作为替代方案,我们开发并一直在改进 AspectInjector — 一个框架,它允许在编译时应用方面,并具有简单但灵活的界面。
Using the Code
让我们快速浏览一下 AspectInjector
可以轻松完成的工作。您可以使用 Package Manager 控制台中的以下命令将库添加到您的项目中
Install-Package AspectInjector -Pre
目前只有预发布版本拥有完整的功能集,因此需要 "-Pre
" 开关才能获得 Aspect Injector 最完整的功能。
假设我们需要一个简单的跟踪所有方法的方法 - 只需捕获方法开始和结束。为了完成此操作,您需要定义一个实现所需行为的方面
public class TraceAspect
{
[Advice(InjectionPoints.Before, InjectionTargets.Method)]
public void TraceStart([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name)
{
Console.WriteLine("[{0}] Method {1}.{2} started", DateTime.UtcNow, type.Name, name);
}
[Advice(InjectionPoints.After, InjectionTargets.Method)]
public void TraceFinish([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name)
{
Console.WriteLine("[{0}] Method {1}.{2} finished", DateTime.UtcNow, type.Name, name);
}
}
此定义意味着方法 TraceStart
和 TraceFinish
将分别在每个方法的开始和结束时被调用,并且每次调用都将包含有关目标方法名称及其声明类型的信息。
为了将方面应用于任何需要它的类,只需使用 Aspect
属性标记目标类
[Aspect(typeof(TraceAspect))]
public class SampleClass
{
public void Method1()
{
//...
}
public int Method2()
{
//...
}
}
结果,对 TraceStart
和 TraceFinish
的调用将被注入到 SampleClass
的所有方法中。这是反编译后的结果代码
public class SampleClass
{
//Added by AspectInjector
private TraceAspect __a$i_TraceAspect;
public void Method1()
{
//Added by AspectInjector
this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method1");
Console.WriteLine("Inside Method1");
//Added by AspectInjector
this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method1");
}
public int Method2()
{
//Added by AspectInjector
this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method2");
Console.WriteLine("Inside Method2");
//Added by AspectInjector
int num = 1;
int result = num;
this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method2");
return result;
}
public SampleClass()
{
//Added by AspectInjector
this.__a$_initializeInstanceAspects();
}
//Added by AspectInjector
private void __a$_initializeInstanceAspects()
{
if (this.__a$i_TraceAspect == null)
{
this.__a$i_TraceAspect = new TraceAspect();
}
}
}
这是一个最简单的例子之一,它没有展示 AspectInjector
的所有功能。关于注入目标、目标成员过滤(由 Aspect
属性上的附加参数控制)和建议参数来源,还有更多选项。所有这些信息都可以在 文档中找到。
有一些功能值得特别关注,例如 "Around" 注入点。它目前仅在 Aspect
Injector 的预发布版本中可用,并允许完全包装任何调用。这里最简单的例子之一是跟踪调用持续时间
public class DurationAspect
{
[Advice(InjectionPoints.Around, InjectionTargets.Method)]
public object TraceDuration([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name,
[AdviceArgument(AdviceArgumentSource.Target)] Func<object[], object> target,
[AdviceArgument(AdviceArgumentSource.Arguments)] object[] arguments)
{
var sw = new Stopwatch();
sw.Start();
var result = target(arguments);
sw.Stop();
Console.WriteLine("[{0}] Method {1}.{2} took {3} ms",
DateTime.UtcNow, type.Name, name, sw.ElapsedMilliseconds);
return result;
}
}
主要思想是,带有 InjectionPoints.Around
的建议必须接受函数委托和相应的参数——所有必要的信息才能进行原始调用。Advice
方法可以在调用原始方法之前和之后执行任何额外的工作,如示例所示。但是,就代码更改而言,“around
”注入比“before
”和“after
”更重。后两者修改原始方法的主体,而“around
”创建了额外的包装函数,因此建议仅在需要在方法的开始和结束之间传递某些数据时使用。
另一个重要的功能是接口注入。您可能不仅需要将额外的代码注入到某些方法、属性或事件中,还需要将额外的接口注入到一个类中。一个经典的例子是 .NET UI 框架的 INotifyPropertyChanged
[AdviceInterfaceProxy(typeof(INotifyPropertyChanged))]
public class NotifyPropertyChangedAspect : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
[Advice(InjectionPoints.After, InjectionTargets.Setter)]
public void RaisePropertyChanged(
[AdviceArgument(AdviceArgumentSource.Instance)] object targetInstance,
[AdviceArgument(AdviceArgumentSource.Name)] string propertyName)
{
PropertyChanged(targetInstance, new PropertyChangedEventArgs(propertyName));
}
}
之后,您将不得不使用 [Aspect(typeof(NotifyPropertyChangedAspect))]
属性装饰所有视图模型类,并且它们的所有属性将自动变为可通知的。
您可以在 项目页面上找到有关可用属性和参数的更多详细信息。我们很乐意收到您对 AspectInjector
的任何反馈和建议!
历史
- 2016 年 8 月 16 日:初始版本