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
- {
- ...
- }
注射
Injection 是选择要注入的Aspect,如果我们的Aspect 跟最终操作的Attribute 是同一个的话,Injection 的Aspect 就是同一个。
点击(此处)折叠或打开
- [Aspect(Scope.Global)]
- [Injection(typeof(LoggingAspectAttribute))]
- public class LoggingAspectAttribute : Attribute
- {
- ...
- }
点击(此处)折叠或打开
- [Aspect(Scope.Global)]
- public class LoggingAspect
- {
- ...
- }
- [Aspect(Scope.Global)]
- public class MeasurementAspect
- {
- ...
- }
- [Injection(typeof(LoggingAspect))]
- [Injection(typeof(MeasurementAspect))]
- public class LoggingAttribute : Attribute
- {
- }
忠告
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();
- }
- }
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 就会依照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)));
- }
- }
- }
点击(此处)折叠或打开
- [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));
- }
- }
点击(此处)折叠或打开
- [NotifyAspect]
- public class MainWindowViewModel
- {
- public MainWindowViewModel()
- {
- this.MyText = "Hello World";
- }
- public string MyText { get; set; }
- }
类似的提示在开发过程中都会不时地跳出来,跟着提示去处理,几乎不会踩到雷,这样一个用心的AOP 框架,推荐给各位朋友。