.NET靜態程式碼編織——肉夾饃(Rougamo)4.0

nigture發表於2024-08-12

肉夾饃(https://github.com/inversionhourglass/Rougamo),一款編譯時AOP元件。相比動態代理AOP需要在應用啟動時進行初始化,編譯時完成程式碼編織的肉夾饃減少了應用啟動初始化的時間,同時肉夾饃還支援所有種類的方法,無論方法是同步還是非同步、靜態還是例項、構造方法還是屬性都是支援的。肉夾饃無需初始化,編寫好切面型別後直接應用到對應方法上即可,同時肉夾饃還提供了方法特徵匹配和類AspectJ表示式匹配的批次應用規則。

非同步切面

得益於3.0對切面實現方式的改變,4.0版本基於代理織入實現了非同步切面功能。那麼什麼是非同步切面呢?直白的說就是新增了OnEntry/OnSuccess/OnException/OnExit對應的非同步方法OnEntryAsync/OnSuccessAsync/OnExceptionAsync/OnExitAsync

如何使用

要編寫非同步切面,一般繼承AsyncMoAttributeAsyncMo,然後重寫OnXxxAsync方法即可。

// 定義切面型別
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) { }
}

public class Cls
{
    // 應用到同步方法上
    [Test]
    public void M() { }

    // 應用到非同步方法上
    [Test]
    public static async Task MAsync() => Task.Yield();
}

聊聊細節

在瞭解的非同步切面的使用方式後你可能有一個疑問:同步切面在非同步方法上的表現和非同步切面在同步方法上的表現是什麼樣的?回答這個問題前,我們可以先看一下AsyncMoAttribute的原始碼:

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

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

    public override ValueTask OnSuccessAsync(MethodContext context) => default;

    public override ValueTask OnExitAsync(MethodContext context) => default;

    public sealed override void OnEntry(MethodContext context)
    {
        OnEntryAsync(context).ConfigureAwait(false).GetAwaiter().GetResult();
    }

    public sealed override void OnException(MethodContext context)
    {
        OnExceptionAsync(context).ConfigureAwait(false).GetAwaiter().GetResult();
    }

    public sealed override void OnSuccess(MethodContext context)
    {
        OnSuccessAsync(context).ConfigureAwait(false).GetAwaiter().GetResult();
    }

    public sealed override void OnExit(MethodContext context)
    {
        OnExitAsync(context).ConfigureAwait(false).GetAwaiter().GetResult();
    }
}

從原始碼中可以看到,AsyncMoAttribute是包含同步切面方法的,同時還有預設實現,實現程式碼就是直接呼叫非同步切面方法,然後GetResult。同樣的,如果你去看MoAttribute的原始碼,你就會發現,MoAttribute同樣擁有非同步切面方法,並且預設實現就是呼叫同步切面方法。所以,關於上面那個問題的答案就是:在同步方法中將呼叫同步切面方法,在非同步方法中將呼叫非同步切面方法。

此時你可能會有另一個疑問:既然AsyncMoAttributeMoAttribute都擁有全部的同步切面方法和非同步切面方法沒什麼還要分兩個類呢?

這是綜合便捷性和安全性考慮後的設計。正如前面所說,肉夾饃會在同步方法中將呼叫同步切面方法,在非同步方法中將呼叫非同步切面方法。如果不分開為兩個類繼續使用MoAttribute,那麼首先一個問題:MoAttribute中的所有切面方法是應該設計為抽象方法讓子類實現全部同步非同步切面方法,還是設計為帶有預設實現的虛方法讓子類自由選擇重寫方法?

  • 選擇設計為抽象方法
    設計為抽象方法就增加了子類在繼承時的額外工作,需要實現所有的切面方法。

  • 選擇設計為帶有預設實現的虛方法
    選擇這種方法就面臨另一個問題:預設實現採用空方法實現,還是採用非同步切面與同步切面的互呼叫(在非同步切面方法中預設呼叫同步切面方法,在同步切面方法中預設呼叫非同步切面方法)

    • 採用空方法實現
      由於是虛方法,所以子類在繼承MoAttribute時並不是必須重寫虛方法,所以如果重寫了某個同步切面方法但是沒有重寫對應的非同步切面方法,那麼就會導致該切面型別在應用到同步方法上和非同步方法上會有不同的表現,這往往是不符合預期的。
    • 採用非同步切面與同步切面的互呼叫
      和採用空方法實現存在同樣的問題,但後果卻更嚴重。比如如果在繼承MoAttribute時因為不需要在方法退出時做任何操作,所以既沒有重寫OnExit也沒有重寫OnExitAsync,那麼在方法退出時呼叫OnExitOnExitAsync時就會出現OnExitOnExitAsync遞迴呼叫。

從上面的說明,你應該能理解將同步切面和非同步切面分為兩個型別的原因了。如果你觀察細緻,你可能已經發現上面AsyncMoAttribute原始碼中的同步切面方法還增加了sealed關鍵字,這也是為了增加安全性,禁止重寫,避免在重寫方法時因IDE智慧提示重寫了同步切面方法而又沒有重寫對應的非同步方法,導致出現同步切面方法和非同步切面方法表現不一致的問題。

完全自定義的RawMoAttribute

既然MoAttributeAsyncMoAttribute的預設實現是直接呼叫對應的方法,那麼如果我覺得預設的實現不是最優呢,比如AsyncMoAttribute預設的同步切面是直接呼叫非同步切面然後同步等待完成,我有更好的同步方案,應該怎麼做呢,畢竟同步切面方法都透過sealed關鍵字禁止重寫了。

細心的你在檢視上面AsyncMoAttribute原始碼時可能已經發現了,AsyncMoAttribute繼承自RawMoAttribute,同樣的MoAttribute也繼承自RawMoAttributeRawMoAttribute開放了所有同步非同步切面方法,這些方法都是抽象方法,你可以完全自定義同步非同步切面。在繼承RawMoAttribute實現同步非同步切面方法時需要注意前面提到的:避免同步切面和非同步切面的程式碼邏輯有差異,避免同步切面方法和非同步切面方法出現遞迴呼叫。

其他更新內容

新增型別

除了上面提到的AsyncMoAttributeRawMoAttribute,還新增了RawMO,MO,AsyncMo分別與RawMoAttribute,MoAttribute,AsyncMoAttribute對應,區別在於後者繼承自Attribute。因為肉夾饃的應用方式除了Attribute應用,還可以透過 實現空介面IRougamo<> 的方式來應用,這種方式並不需要型別是Attribute子類,當然Attribute子類也是接受的,這裡只是提供了不繼承Attribute的選擇。

效能最佳化之強制同步

在介紹非同步切面時有說到:同步方法會呼叫同步切面,非同步方法會呼叫非同步切面。這一設定在預設情況是很好的設定,但如果你的切面操作完全不涉及非同步操作,那麼在非同步方法中實際並不需要呼叫非同步切面,因為非同步切面走了一層ValueTask包裝,相比同步切面會存在額外的開銷。在這種情況下,可以透過ForceSync屬性設定在非同步方法中需要強制執行同步切面的方法:

public class TestAttribute : MoAttribute
{
    // 在非同步方法中,OnEntry和OnExit將強制呼叫同步切面方法
    public override ForceSync ForceSync => ForceSync.OnEntry | ForceSync.OnExit;
}

其實,如果對效能要求並不是那麼嚴格,是可以不去設定ForceSync的,肉夾饃已採用ValueTask,預設的非同步切面方法對同步切面方法包裝的額外開銷十分有限。

async void的特別說明

在3.0釋出時便有聊到,3.0後採用的代理織入方式對async void的支援可能與你的預期效果不同。具體請跳轉檢視 async void特別說明

考慮到async void是不推薦的使用方式(官方也不推薦),所以決定不對async void做更多的適配工作,繼續沿用3.0的做法,將async void方法當做普通的void返回值的同步方法看待。但與3.0不同的是,在4.0版本中如果發現async void方法上應用了肉夾饃切面型別,將在編譯時產生一個MSBuild告警。告警資訊往往不容易引起注意,如果你確定自己並不希望async void上應用肉夾饃切面型別,或者你希望子出現這種情況時能提醒你讓你做相應的修改,那麼你可以在專案檔案的PropertyGroup節點下新增一個子節點<FodyTreatWarningsAsErrors>true</FodyTreatWarningsAsErrors>,這個配置會讓Fody產生的告警資訊變為錯誤資訊,從而使得編譯失敗達到提醒的目的。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <FodyTreatWarningsAsErrors>true</FodyTreatWarningsAsErrors>
  </PropertyGroup>
</Project>

MethodContext成員變化

刪除MethodContext中的IsAsync, IsIterator, MosNonEntryFIFO, Data屬性,將RealReturnType標記為過時並隱藏,同時新增TaskReturnType屬性,該屬性與RealReturnType具有類似功能。

配置檔案智慧提示

肉夾饃有些許可配置項,這些配置項可在FodyWeavers.xml中配置,詳見 配置項。現在為這些配置增加了對應的xml schema,在修改FodyWeavers.xmlRougamo節點時會出現智慧提示。

相關文章