- A+
上文介绍到Aspect
是Metalama
的核心概念,它本质上是一个编译时的AOP切片。下面我们就来系统说明一下Metalama
中的Aspect
。
Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架
本文讲些什么
- 关于Metalama中Aspect的基础
- 一些关于Aspect的示例,最终目的是通过本篇的介绍,将在编译时自动为类型添加
INotifyPropertyChanged
,实现如下效果:- 自动添加接口
- 自动添加接口实现
- 改写属性的set和get
关于Aspect
在前面的文章中我们已经介绍了使用Metalama
编写简单的AOP。但是例子过于简单,也只是在代码前后加了两个Console.WriteLine
,并没有太大的实际参考意义。下面我就以几个实际例子,来体现Metalama
在复用代码方面的好处。
对于Metalama
中的Aspect
分为以下两种API
1.Aspect基础API
- TypeAspect 对类型进行编译时代码插入,见示例3
- MethodAspect
- PropertyAspect
- ParameterAspect
- EventAspect
- FieldAspect
- FieldOrPropertyAspect
- ConstructorAspect
2.Override API(重写式API)
重写试API使用更方便、更直观,与上面基础API等价,但是更容易使用
- OverrideMethodAspect 对方法进行编译时代码插入,请见下面示例1
- OverrideFieldOrPropertyAspect 对字段或属性进行编译时代码插入,请见下面示例2
- OverrideEventAspect 对事件进行编译时插入代码
以 MethodAspect
和 OverrideMethodAspect
为例,以下代码等价。
基础API MethodAspect
public class LogAttribute : MethodAspect { public override void BuildAspect(IAspectBuilder<IMethod> builder) { // 为方法添加重写 builder.Advices.OverrideMethod(builder.Target,nameof(this.MethodLog)); } [Template]// 这个Template必须要加 public dynamic MethodLog() { Console.WriteLine(meta.Target.Method.ToDisplayString() + " 开始运行."); var result = meta.Proceed(); Console.WriteLine(meta.Target.Method.ToDisplayString() + " 结束运行."); return result; } }
Override API
public class LogAttribute : OverrideMethodAspect { public override dynamic? OverrideMethod() { Console.WriteLine(meta.Target.Method.ToDisplayString() + " 开始运行."); var result = meta.Proceed(); Console.WriteLine(meta.Target.Method.ToDisplayString() + " 结束运行."); return result; } }
下面针对各种情况举一些试例。
根据每个例子的不同也分别介绍如何对方法、字段、属性进行重写。
关于meta类
通过上面的示例我们可以看到,无论是在基础API
中还是Override API
中,在定义AOP方法时,都使用到了meta
。 meta
是一个方便在Aspect
中访问当前AOP上下文的工具类
常用的成员有:
成员 | 说明 |
---|---|
meta.Proceed() |
等同于执行AOP作用目标直接执行,例如方法Aspect中就是原方法直接执行,属性的get中就是获取值,属性的Set中就是赋值value |
meta.Target |
当前AOP的作用目标,如作用目标是个方法则通过 meta.Target.Method 调用,如果目标是个属性则通过 meta.Target.Propery 调用 |
meta.This |
等同于使用在AOP作用目标中的this ,例如可以用于获取AOP目标所在类的其它属性,方法 |
meta.ThisStatic |
用于访问AOP作用目标中的静态类型 |
示例1对方法:实现一个重试N次的功能
在平时的代码中,有这种场景,例如,我调用一个方法或API,他有一定的概率失败,例如发生了网络异常,所以我们就要设定一个重试机制(以重试3次然后放弃为例)。
假设我们有一个方法,代码详见示例中的RetryDemo
。
static int _callCount; // 此方法第一二次调用会失败,第三次会成功 static void MyMethod() { _callCount++; Console.WriteLine($"当前是第{_callCount}次调用."); if (_callCount <= 2) { Console.WriteLine("前两次直接抛异常:-("); throw new TimeoutException(); } else { Console.WriteLine("成功 :-)"); } }
如果我们直接编写代码,可以使用类似以下逻辑处理。
for (int i = 0; i < 3; i++) { try { MyMethod(); break; } catch (Exception ex) { // Console.WriteLine(ex); } }
这样的话,对于不同的方法我们就会出现大量的重试逻辑。
那么使用Metalama
我们如何进行代码改造,去掉复用代码呢。
第一步,我们需要创建一个可以修改方法的AOP的Attribute
,如下:
internal class RetryAttribute : OverrideMethodAspect { // 重试次数 public int RetryCount { get; set; } = 3; // 应用到方法的切面模板 public override dynamic? OverrideMethod() { for (var i = 0; ; i++) { try { return meta.Proceed(); // 这是实际调用方法的位置 } catch (Exception e) when (i < this.RetryCount) { Console.WriteLine($"发生异常 {e.Message.GetType().Name}. 1秒后重试."); Thread.Sleep(1000); } } } }
这里可以看到定义这个Attribute时,使用了Metalama
提供的基类OverrideMethodAspect
此基类是用于为方法添加编译时切面代码的Attribute
.
然后我们将这个Attribute
加到方法定义上。
static int _callCount; [Retry(RetryCount = 5)] static void MyMethod() { _callCount++; Console.WriteLine($"当前是第{_callCount}次调用."); if (_callCount <= 2) { Console.WriteLine("前两次直接抛异常:-("); throw new TimeoutException(); } else { Console.WriteLine("成功 :-)"); } }
这样在编译时Metalama
就会将代码编译为如下图所示。
而RetryAttribute
编译后则会变为
也就是会将原有的OverrideMethod
自动实现为throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.")
。
最终调用结果为
当前是第1次调用. 前两次直接抛异常:-( 发生异常 String. 1秒后重试. 当前是第2次调用. 前两次直接抛异常:-( 发生异常 String. 1秒后重试. 当前是第3次调用. 成功 :-)
源代码:https://github.com/chsword/metalama-demo/tree/main/src/RetryDemo
示例2对属性:INotifyPropertyChanged自动属性的实现
在很多处理逻辑中我们会用到INotifyPropertyChanged
如我们要获取以下类的属性更改:
public class MyModel { public int Id { get; set; } public string Name { get; set; } }
我们可以这么做:
using System.ComponentModel; public class MyModel: INotifyPropertyChanged { private int _id { get; set; } public int Id { get { return _id; } set { if (this._id != value) { this._id = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id")); } } } private string _name; public string Name { get { return _name; } set { if (this._name != value) { this._name = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name")); } } } public event PropertyChangedEventHandler? PropertyChanged; }
但是这里,要将自动属性进行展开,并产生大量字段,对于这里的重复代码,我们可以用Metalama进行处理
,我们最终要代码实现为如下:
public class MyModel: INotifyPropertyChanged { [NotifyPropertyChanged] public int Id { get; set; } [NotifyPropertyChanged] public string Name { get; set; } public event PropertyChangedEventHandler? PropertyChanged; }
当然我们也要实现NotifyPropertyChangedAttribute
:
public class NotifyPropertyChangedAttribute : OverrideFieldOrPropertyAspect { public override dynamic OverrideProperty { // 保留原本get的逻辑 get => meta.Proceed(); set { // 判断当前属性的Value与传入value是否相等 if (meta.Target.Property.Value != value) { // 原本set的逻辑 meta.Proceed(); // 这里的This等同于调用类的This meta.This.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(meta.Target.Property.Name)); } } } }
这样就可以实现上面相同的效果。
源代码:https://github.com/chsword/metalama-demo/tree/main/src/PropertyDemo
示例3对类型:进一步实现INotifyPropertyChanged自动属性
刚才对属性在编译时生成INotifyPropertyChanged
实现的代码中,其实可以再进一步优化,INotifyPropertyChanged
接口的实现也可以通过Metalama
进一步省去,最终代码为:
[TypeNotifyPropertyChanged] public class MyModel { public int Id { get; set; } public string Name { get; set; } }
那么TypeNotifyPropertyChangedAttribute
又应该怎么实现呢,Type Aspect并没有对应的Override实现,所以要使用TypeAspect。
internal class TypeNotifyPropertyChangedAttribute : TypeAspect { public override void BuildAspect(IAspectBuilder<INamedType> builder) { // 当前类实现一个接口 builder.Advices.ImplementInterface(builder.Target, typeof(INotifyPropertyChanged)); // 获取所有符合要求的属性 var props = builder.Target.Properties.Where(p => !p.IsAbstract && p.Writeability == Writeability.All); foreach (var property in props) { //用OverridePropertySetter重写属性或字段 //参数1 要重写的属性 参数2 新的get实现 参数3 新的set实现 builder.Advices.OverrideFieldOrPropertyAccessors(property, null, nameof(this.OverridePropertySetter)); } } // Interface 要实现什么成员 [InterfaceMember] public event PropertyChangedEventHandler? PropertyChanged; // 也可以没有这个方法,直接调用 meta.This 这里只是展示另一种调用方式,更加直观 [Introduce(WhenExists = OverrideStrategy.Ignore)] protected void OnPropertyChanged(string name) { this.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(name)); } // 重写set的模板 [Template] private dynamic OverridePropertySetter(dynamic value) { if (value != meta.Target.Property.Value) { meta.Proceed(); this.OnPropertyChanged(meta.Target.Property.Name); } return value; } }
这样就可以实现和以上相同效果的代码,以后再添加实现INotifyPropertyChanged
的类,只要添加以上Attribute即可。
源代码:https://github.com/chsword/metalama-demo/tree/main/src/TypeDemo
减少代码入侵
上面的示例3中,其实对方法还是有一定入侵的,至少要标记一个Attribute,Metalama
还提供了其它无入侵的方式来为类或方法添加Aspect
,我们将在后面来介绍。
先上个代码
internal class Fabric : ProjectFabric { public override void AmendProject(IProjectAmender amender) { // 添加 TypeNotifyPropertyChangedAttribute 到符合规则的类上 // 当前筛选以 Model 结尾的本项目中的类型添加 TypeNotifyPropertyChangedAttribute amender.WithTargetMembers(c => c.Types.Where(t => t.Name.EndsWith("Model")) ).AddAspect(t => new TypeNotifyPropertyChangedAttribute()); } }
调试
调试 Aspect 的 Attribute
时,尚不能使用断点直接调试,但可以通过以下方法:
在编译配置中除Debug
或Release
外还有一个LamaDebug
。选择使用LamaDebug
即可直接对Metalama
的项目进行调试。
- 在编译时就会调用的内容中,如BuildAspect,使用
System.Diagnostics.Debugger.Break()
. - 在Template方法或Override中, 使用
meta.DebugBreak
。
如果是想以附加进程等方式添加断点调试,可以参考官方文档https://doc.metalama.net/aspects/debugging-aspects