C# Open Source 的Compile-time AOP 框架- AspectInjector

看到Bill叔在twMVC#39讲的Compile-time weaving的AOP框架- PostSharp,勾起了我的AOP魂,随手Google了一下,让我找到了一个Open Source的框架- AspectInjector(看名字我还以为是某个Dependency Injection的套件),看它在GitHub的介绍里面下了postsharp的标签,似乎有向PostSharp看齐的目标。

AOP的概念我就不多做解释了,之前有尝试使用过PostSharp一阵子,但是对我来说CP值不高,所以之后一直都是使用Castle DynamicProxy来实作AOP,不过Castle DynamicProxy除了效能耗损之外,另外一个比较大的问题是无法直接针对单一方法做Proxy,而AspectInjector则没有这些副作用,虽然不如PostSharp那样的精致,但是对我所面临的需求而言,已经非常够用了。

接着介绍一下我的实验情境,我需要一个LoggingAspect来帮我记录目标方法的名稱、參數、回傳值,那么以一般的拦截器来说,能取得方法的名称、参数、回传值,大致上就能做一些事来满足需求了。

方面
我们就从零开始,用AspectInjector打造一个LoggingAspect,并且在这个过程中,一一解释AspectInjector所提供的各种Attribute的作用,首先就是Aspect。

Aspect 是定义一个拦截器,它只能是类别。



    [Aspect(Scope.Global)]
    public class LoggingAspectAttribute : Attribute
    {
        ...
    }


Scope参数是一个列举型别,有Global及PerInstance,选择Global的话,Aspect会被建成Singleton;选择PerInstance的话,Aspect的生命周期就会随着被拦截的目标方法而生灭。
注入
Injection 是选择要注入的Aspect,如果我们的Aspect 跟最终操作的Attribute 是同一个的话,Injection 的Aspect 就是同一个。



    [Aspect(Scope.Global)]
    [Injection(typeof(LoggingAspectAttribute))]
    public class LoggingAspectAttribute : Attribute
    {
        ...
    }


Inject 的Aspect 可以有多个,Aspect 也可以另外宣告,最后再透过Injection 合在一起,像这样:

[Aspect(Scope.Global)]
public class LoggingAspect
{
    ...
}

[Aspect(Scope.Global)]
public class MeasurementAspect
{
    ...
}

[Injection(typeof(LoggingAspect))]
[Injection(typeof(MeasurementAspect))]
public class LoggingAttribute : Attribute
{
}

以上面这个例子来说,最终操作的是Logging 这个Attribute,它会组合注入的两个Aspect,而这样的设计方式让Aspect 规划上可以更有弹性。
忠告
Advice 是定义最终要与目标方式缝合(Weaving)的方法



    [Aspect(Scope.Global)]
    [Injection(typeof(LoggingAspectAttribute))]
    public class LoggingAspectAttribute : Attribute
    {
        [Advice(Kind.Before, Targets = Target.Method)]
        public void Before()
        {
            throw new NotImplementedException();
        }

        [Advice(Kind.After, Targets = Target.Method)]
        public void After()
        {
            throw new NotImplementedException();
        }

        [Advice(Kind.Around, Targets = Target.Method)]
        public object Around()
        {
            throw new NotImplementedException();
        }
    }


AdviceAttribute有两个属性,Kind及Targets,都是列举型别。

Kind:有三个列举值Before、After、Around,分别是目標方法執行前、目標方法執行後、包著目標方法執行(需手動執行目標方法)。
Targets:限定特定的目标方法,这个列举值就多了,我就不一一列出来了,它最主要的作用是定义Advice可以与具有什么样特性的目标方法缝合,预设值是Any,就是不限定。
缝合顺序
中间我安插一个篇幅来说明当多个Injection 加上多个Advice 时,它的缝合顺序会是怎么样?原则上Before → Around → After 这个顺序是不变的,当多个相同类型的Advice 时,规则是这样的,依Attribute 标记的顺序,由上而下:

Before:依序将Advice插入在目标方法的最上面。
Around:依序将Advice 往內包装目标方法。
After:依序将Advice插入在目标方法的最下面。
底下有一个示意图,辅助我的说明。

多个Injection 加上多种类型的Advice 时,原则没变,先按照Advice 类型的顺序,再按照Injection 的顺序由上而下与目标方法缝合。

基本上,把握住缝合顺序的原则,就不会乱了。

论据
Argument用来定义取得目标方法的资讯,包括名称、参数、回传值、目标方法实例、…等,需要传入一个Source参数,也是一个列举型别,用来定义参数的类型。



    [Aspect(Scope.Global)]
    [Injection(typeof(LoggingAspectAttribute))]
    public class LoggingAspectAttribute : Attribute
    {
        [Advice(Kind.Before, Targets = Target.Method)]
        public void Before([Argument(Source.Name)]string name, [Argument(Source.Arguments)]object[] arguments)
        {
            Console.WriteLine("On Before");
        }

        [Advice(Kind.After, Targets = Target.Method)]
        public void After([Argument(Source.Name)] string name, [Argument(Source.Arguments)] object[] arguments, [Argument(Source.ReturnValue)] object returnValue)
        {
            Console.WriteLine("On After");
        }

        [Advice(Kind.Around, Targets = Target.Method)]
        public object Around(
            [Argument(Source.Name)] string name,
            [Argument(Source.Arguments)] object[] arguments,
            [Argument(Source.Target)] Func<object[], object> target)
        {
            Console.WriteLine("On Around Before");

            var result = target(arguments);

            Console.WriteLine("On Around After");

            return result;
        }
    }


大致上这样就完成了,我们就可以把LoggingAspect 标记在我们的目标方法上,这样就会在建置的时候,把Advice 跟目标方法缝起来。

以下是执行结果

如果要套用到整个目标类别上,就直接将LoggingAspect 标记在目标类别就好了,这样Advice 就会依照Targets 属性,来决定要不要缝进目标类别内的方法里面。

非同步方法
在 AspectInjector 的 GitHub 上有一小段字引起了我的注意:

您还可以具有After(异步感知)和Around(环绕/代替)类型

重点是async-aware这个字- 感知非同步,试了一下,果真非同步方法也能支援,Aspect的程式码一个字都不用改,而且Advice的缝合顺序都没有乱。
Mixin
Mixin 可以将介面及介面的实作混进Aspect,让有标记Aspect 的目标类别直接是实作好该介面的状态,举个例子,有在WPF 或Xamarin.Forms 实作过MVVM 模式朋友,应该都对INotifyPropertyChanged 这个介面不陌生。

这个介面的实际作用我不多介绍,有兴趣的朋友就自行Google,它的绑定机制所引发的问题是造成大量重覆的程式碼,AOP就可以用来消除这些重覆的程式码,而Mixin的设计又让整个过程变得更简便。

来看个范例,假定我的WPF 应用程式上有一个TextBlock,与MainWindowViewModel 的MyText 属性做OneWay 绑定,指定用PropertyChanged 的方式来做更新,MyText 的初始值为"Hello World",我有一个Button 会去将MyText 的值改为"abc"。

我们的MainWindowViewModel必须实作INotifyPropertyChanged介面,并且在MyText的Setter中呼叫PropertyChanged事件,与MyText相关的绑定才会生效。



    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private string myText;

        public MainWindowViewModel()
        {
            this.MyText = "Hello World";
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public string MyText
        {
            get => this.myText;
            set
            {
                this.myText = value;
                this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.MyText)));
            }
        }
    }


所以,我们就可以想像到为什么会有大量重覆的程式码,因为一个被绑定的Property就要写一次,而Mixin可以帮我们省掉这些工作,底下我们就用Mixin制作一个NotifyAspectAttribute,关键的地方在于NotifyAspectAttribute要实作INotifyPropertyChanged,并且用MixinAttribute将INotifyPropertyChanged混进来。



    [AttributeUsage(AttributeTargets.Class)]
    [Injection(typeof(NotifyAspectAttribute))]
    [Aspect(Scope.PerInstance)]
    [Mixin(typeof(INotifyPropertyChanged))]
    public class NotifyAspectAttribute : Attribute, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        [Advice(Kind.After, Targets = Target.Public | Target.Setter)]
        public void AfterSetter([Argument(Source.Instance)] object sender, [Argument(Source.Name)] string propertyName)
        {
            this.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
        }
    }


然后我们就在MainWindowViewModel上标记[NotifyAspect],就大功告成了,可以看到MainWindowViewModel的程式码看起来舒服很多了,而且NotifyAspectAttribute是可以重覆利用的,放到任何的ViewModel都有作用,这个就是Mixin的威力。



    [NotifyAspect]
    public class MainWindowViewModel
    {
        public MainWindowViewModel()
        {
            this.MyText = "Hello World";
        }

        public string MyText { get; set; }
    }


另外,我觉得AspectInjector 有一点做得很好,就是它在开发时期的提示,举个例子,我如果宣告Around Advice 是一个void,它就提示为错误,而且建置不会过。

类似的提示在开发过程中都会不时地跳出来,跟着提示去处理,几乎不会踩到雷,这样一个用心的AOP 框架,推荐给各位朋友。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值