Metalama利用Aspect在编译时实现进行消除重复代码

发布时间:2022-04-12 14:29

上文介绍到Aspect是Metalama的核心概念,它本质上是一个编译时的AOP切片。下面我们就来系统说明一下Metalama中的Aspect。

关于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上下文的工具类

常用的成员有:

图片[1] - Metalama利用Aspect在编译时实现进行消除重复代码 - 尘心网

示例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就会将代码编译为如下图所示。

图片[2] - Metalama利用Aspect在编译时实现进行消除重复代码 - 尘心网
而RetryAttribute编译后则会变为

图片[3] - Metalama利用Aspect在编译时实现进行消除重复代码 - 尘心网
也就是会将原有的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

文档下载:Metalama利用Aspect在编译时实现进行消除重复代码.doc文档

THE END
喜欢就支持一下吧