Chinaunix首页 | 论坛 | 博客
  • 博客访问: 155893
  • 博文数量: 11
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 170
  • 用 户 组: 普通用户
  • 注册时间: 2020-07-22 17:26
个人简介

blender https://www.blender-3d.cn/ blender3D模型库

文章分类

全部博文(11)

文章存档

2020年(11)

我的朋友

分类: C#/.net

2020-07-24 16:30:53

看到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 是定义一个拦截器,它只能是类别。

点击(此处)折叠或打开

  1. [Aspect(Scope.Global)]
  2. public class LoggingAspectAttribute : Attribute
  3. {
  4.     ...
  5. }
Scope参数是一个列举型别,有Global及PerInstance,选择Global的话,Aspect会被建成Singleton;选择PerInstance的话,Aspect的生命周期就会随着被拦截的目标方法而生灭。
注射
Injection 是选择要注入的Aspect,如果我们的Aspect 跟最终操作的Attribute 是同一个的话,Injection 的Aspect 就是同一个。

点击(此处)折叠或打开

  1. [Aspect(Scope.Global)]
  2. [Injection(typeof(LoggingAspectAttribute))]
  3. public class LoggingAspectAttribute : Attribute
  4. {
  5.     ...
  6. }
Inject 的Aspect 可以有多个,Aspect 也可以另外宣告,最后再透过Injection 合在一起,像这样:

点击(此处)折叠或打开

  1. [Aspect(Scope.Global)]
  2. public class LoggingAspect
  3. {
  4.     ...
  5. }

  6. [Aspect(Scope.Global)]
  7. public class MeasurementAspect
  8. {
  9.     ...
  10. }

  11. [Injection(typeof(LoggingAspect))]
  12. [Injection(typeof(MeasurementAspect))]
  13. public class LoggingAttribute : Attribute
  14. {
  15. }
以上面这个例子来说,最终操作的是Logging 这个Attribute,它会组合注入的两个Aspect,而这样的设计方式让Aspect 规划上可以更有弹性。
忠告
Advice 是定义最终要与目标方式缝合(Weaving)的方法

点击(此处)折叠或打开

  1. [Aspect(Scope.Global)]
  2. [Injection(typeof(LoggingAspectAttribute))]
  3. public class LoggingAspectAttribute : Attribute
  4. {
  5.     [Advice(Kind.Before, Targets = Target.Method)]
  6.     public void Before()
  7.     {
  8.         throw new NotImplementedException();
  9.     }

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

  15.     [Advice(Kind.Around, Targets = Target.Method)]
  16.     public object Around()
  17.     {
  18.         throw new NotImplementedException();
  19.     }
  20. }
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参数,也是一个列举型别,用来定义参数的类型。

点击(此处)折叠或打开

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

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

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

  22.         var result = target(arguments);

  23.         Console.WriteLine("On Around After");

  24.         return result;
  25.     }
  26. }
大致上这样就完成了,我们就可以把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相关的绑定才会生效。

点击(此处)折叠或打开

  1. public class MainWindowViewModel : INotifyPropertyChanged
  2. {
  3.     private string myText;

  4.     public MainWindowViewModel()
  5.     {
  6.         this.MyText = "Hello World";
  7.     }

  8.     public event PropertyChangedEventHandler PropertyChanged;

  9.     public string MyText
  10.     {
  11.         get => this.myText;
  12.         set
  13.         {
  14.             this.myText = value;
  15.             this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.MyText)));
  16.         }
  17.     }
  18. }
所以,我们就可以想像到为什么会有大量重覆的程式码,因为一个被绑定的Property就要写一次,而Mixin可以帮我们省掉这些工作,底下我们就用Mixin制作一个NotifyAspectAttribute,关键的地方在于NotifyAspectAttribute要实作INotifyPropertyChanged,并且用MixinAttribute将INotifyPropertyChanged混进来。

点击(此处)折叠或打开

  1. [AttributeUsage(AttributeTargets.Class)]
  2. [Injection(typeof(NotifyAspectAttribute))]
  3. [Aspect(Scope.PerInstance)]
  4. [Mixin(typeof(INotifyPropertyChanged))]
  5. public class NotifyAspectAttribute : Attribute, INotifyPropertyChanged
  6. {
  7.     public event PropertyChangedEventHandler PropertyChanged;

  8.     [Advice(Kind.After, Targets = Target.Public | Target.Setter)]
  9.     public void AfterSetter([Argument(Source.Instance)] object sender, [Argument(Source.Name)] string propertyName)
  10.     {
  11.         this.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
  12.     }
  13. }
然后我们就在MainWindowViewModel上标记[NotifyAspect],就大功告成了,可以看到MainWindowViewModel的程式码看起来舒服很多了,而且NotifyAspectAttribute是可以重覆利用的,放到任何的ViewModel都有作用,这个就是Mixin的威力。

点击(此处)折叠或打开

  1. [NotifyAspect]
  2. public class MainWindowViewModel
  3. {
  4.     public MainWindowViewModel()
  5.     {
  6.         this.MyText = "Hello World";
  7.     }

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

类似的提示在开发过程中都会不时地跳出来,跟着提示去处理,几乎不会踩到雷,这样一个用心的AOP 框架,推荐给各位朋友。
阅读(1847) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~