Skip to content

异步切面

ihourglass edited this page Dec 16, 2024 · 4 revisions

异步切面

所谓的异步切面,就是为OnEntry/OnSuccess/OnException/OnExit提供了对应的异步方法OnEntryAsync/OnSuccessAsync/OnExceptionAsync/OnExitAsyc

通过继承AsyncMoAttribute,AsyncMo,然后就可以重写对应的异步方法了,这两个类型的区别是AsyncMoAttribute继承了Attribute,而AsyncMo没有。或实现IAsyncMo接口(netstandard 2.1 兼容版本支持)。

public class TestAttribute : AsyncMoAttribute
{
    public override async ValueTask OnEntryAsync(MethodContext) { }
    
    public override async ValueTask OnSuccessAsync(MethodContext) { }
    
    public override async ValueTask OnExceptionAsync(MethodContext) { }
    
    public override async ValueTask OnExitAsync(MethodContext) { }
}

同步切面与异步切面的关系

同步切面类型和异步切面类型均实现IMo接口,IMo接口中包含了所有同步切面方法和异步切面方法的定义,同步切面类型实现IMo接口后会为异步切面方法增加默认实现,同理,异步切面类型实现IMo接口后会为同步切面方法增加默认实现。下面以MoAttributeAsyncMoAttribute为例:

public abstract class MoAttribute : RawMoAttribute
{
    public override void OnEntry(MethodContext context) { }

    // 异步切面方法默认实现,内部直接调用同步切面方法
    public sealed override ValueTask OnEntryAsync(MethodContext context)
    {
        OnEntry(context);
        return default;
    }
}

public abstract class AsyncMoAttribute : RawMoAttribute
{
    public override ValueTask OnEntryAsync(MethodContext context) => default;

    // 同步切面方法默认实现,内部直接调用异步切面方法并阻塞获取结果
    public sealed override void OnEntry(MethodContext context)
    {
        OnEntryAsync(context).ConfigureAwait(false).GetAwaiter().GetResult();
    }
}

当切面类型应用到方法上后,如果方法是同步方法,那么 Rougamo 将织入同步切面方法的调用,如果方法时异步方法,那么 Rougamo 将织入异步切面方法的调用。

// 切面类型应用到同步方法上
[Test]
public void M() { }

// Rougamo 织入后的代码
[Test]
public void M()
{
    var test = new TestAttribute();
    var context = new MethodContext();
    // 调用同步切面方法
    test.OnEntry(context);

    $Rougamo_M();
}

// 将切面类型应用到异步方法上
[Test]
public async Task MAsync() { }

// Rougamo 织入后的代码
[Test]
public async Task MAsync()
{
    var test = new TestAttribute();
    var context = new MethodContext();
    // 调用异步切面方法
    await test.OnEntryAsync(context);

    await $Rougamo_MAsync();
}

通过上面的例子,你应该已经了解同步切面与异步切面之间的关系。同时,你可能也发现了,如果将异步切面类型应用到同步方法上,那么其实无法达到真正的异步效果,内部还是同步阻塞等待异步返回。对于这一情况 Rougamo 也是无可奈何的,目标方法时同步方法,在同步方法中执行异步操作,如果需要异步方法的执行结果,只能同步等待了。但如果你的 AOP 操作可以同时支持同步和异步操作,那么你还可以完全实现IMo的所有同步异步方法。

最佳实践

假设有以下场景,我想要控制某个方法的分布式并发,要在执行方法前获取分布式锁。这里选择使用 Redis 做分布式锁,SDK 选择 StackExchange.Redis,此时你会选择创建一个同步切面类型,还是一个异步切面类型?我的建议是,创建一个通用切面类型,因为StackExchange.Redis对于常用操作分别提供了同步方法和异步方法。

// 以下代码在文本编辑器中编辑,未经验证,经供参考
public class LockAttribute : IMo
{
    private const string KEY = "rougamo";
    private readonly string _token = Guid.NewGuid().ToString();
    private IDatabase _db;
    private bool _took;

    public void OnEntry(MethodContext context)
    {
        // 通过 Rougamo.DI 实现依赖注入,详见 https://github.com/inversionhourglass/Rougamo.DI
        _db = context.GetRequiredService<IDatabase>();
        
        // 同步切面方法中调用同步方法获取锁
        _took = _db.LockTake(KEY, _token, TimeSpan.FromSeconds(3));

        // 锁获取失败,拦截方法执行,返回默认值
        if (!_took) context.ReplaceReturnValue(this, null);
    }

    public async ValueTask OnEntryAsync(MethodContext context)
    {
        _db = context.GetRequiredService<IDatabase>();
        
        // 异步切面方法中调用异步方法获取锁
        _took = await _db.LockTakeAsync(KEY, _token, TimeSpan.FromSeconds(3));

        // 锁获取失败,拦截方法执行,返回默认值
        if (!_took) context.ReplaceReturnValue(this, null);
    }

    public void OnExit(MethodContext context)
    {
        if (!_took) return;

        _db.LockRelease(KEY, _token);
    }

    public async ValueTask OnExitAsync(MethodContext context)
    {
        if (!_took) return;

        await _db.LockReleaseAsync(KEY, _token);
    }

    public void OnSuccess(MethodContext context) { }

    public void OnException(MethodContext context) { }

    public ValueTask OnSuccessAsync(MethodContext context) => default;

    public ValueTask OnExceptionAsync(MethodContext context) => default;
}

如果你的 AOP 操作有同步实现和异步实现,推荐像上面这样针对同步切面方法和异步切面方法分别实现,这样在实际运行时可以达到最佳效果。另外,像上面的例子中,实际只需要实现OnEntryOnExit,直接实现IMo接口还需要针对OnSuccessOnException实现四个空方法,比较繁琐。此时可以直接继承RawMoAttributeRawMo,然后重写用到的方法即可。

切面类型一览

Rougamo 内部定义了多个切面类型方便使用,可以根据自己的需求选择父类/接口,而不必每次都实现最基础的IMo接口。

IMo                        # 切面类型基础接口,所有切面类型都需要实现该接口
├── ISyncMo                # 同步切面接口,通过默认接口方法对异步切面方法添加了默认实现(仅 netstandard2.1 以上版本可用)
├── IAsyncMo               # 异步切面接口,通过默认接口方法对同步切面方法添加了默认实现(仅 netstandard2.1 以上版本可用)
├── RawMoAttribute         # 继承 Attribute,可自定义同步和异步切面方法
│   ├── MoAttribute        # 仅可自定义同步切面方法,异步切面方法使用调用同步切面方法的默认实现
│   └── AsyncMoAttribute   # 仅可自定义异步切面方法,同步切面方法使用调用异步切面方法的默认实现
└── RawMo                  # 与 RawMoAttribute 功能相同,唯一差别是 RawMo 未继承 Attribute
    ├── Mo                 # 与 MoAttribute 功能相同,唯一差别是 Mo 未继承 Attribute
    └── AsyncMo            # 与 AsyncMoAttribute 功能相同,唯一差别是 AsyncMo 未继承 Attribute