65.9K
CodeProject 正在变化。 阅读更多。
Home

又一个面向 .NET 的面向方面编程框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (10投票s)

2016 年 8 月 15 日

CPOL

3分钟阅读

viewsIcon

25019

介绍 .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);
        }
    }

此定义意味着方法 TraceStartTraceFinish 将分别在每个方法的开始和结束时被调用,并且每次调用都将包含有关目标方法名称及其声明类型的信息。

为了将方面应用于任何需要它的类,只需使用 Aspect 属性标记目标类

    [Aspect(typeof(TraceAspect))]
    public class SampleClass
    {
        public void Method1()
        {
		//...
        }

        public int Method2()
        {
		//...
        }    
    }

结果,对 TraceStartTraceFinish 的调用将被注入到 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 日:初始版本
© . All rights reserved.