­

.NET靜態程式碼織入——肉夾饃(Rougamo) 發布1.2.0

肉夾饃(//github.com/inversionhourglass/Rougamo)通過靜態程式碼織入方式實現AOP的組件,其主要特點是在編譯時完成AOP程式碼織入,相比動態代理可以減少應用啟動的初始化時間讓服務更快可用,同時還能對靜態方法進行AOP。

1.0.0 版本中,肉夾饃提供了最基礎的AOP功能,可以進行日誌記錄和APM埋點。在 1.1.0 版本中新增了對更加實用的AOP操作的支援,可以進行異常處理和修改返回值。本次的 1.2.0 版本沒有新增功能,主要是對 1.1.0 版本的增強,新增了一個ExMoAttribute,這個Attribute可能會替代MoAttribute成為大家更常用的Attribute.

前言

這次無法直接從快速開始入手了,了解一下前因後果會讓你對肉夾饃最初的設定和ExMoAttribute的出現所解決的問題有更清晰的認識,這樣也方便後續使用時能夠明確的知道是應該使用MoAttribute還是ExMoAttribute.

在1.1.0版本發布後陸續收到兩個issue,都是回饋在沒有使用async語法的Task/ValueTask返回值的方法無法正確的在方法執行成功(OnSuccess)和方法退出前(OnExit)執行織入的程式碼,比如記錄方法的執行耗時的示例:

static async Task Main(string[] args)
{
    await Test();
}

[Timeline]
static Task Test()
{
    Console.WriteLine($"{nameof(Test)} start");
    return Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine($"{nameof(Test)} end");
    });
}

class TimelineAttribute : MoAttribute
{
    private Stopwatch _stopwatch;

    public override void OnEntry(MethodContext context)
    {
        _stopwatch = Stopwatch.StartNew();
        Console.WriteLine($"{context.Method.Name} {nameof(OnEntry)}");
    }

    public override void OnExit(MethodContext context)
    {
        _stopwatch.Stop();
        Console.WriteLine($"{context.Method.Name} {nameof(OnExit)} - {_stopwatch.ElapsedMilliseconds}ms");
    }
}

期望的輸出可能是下面這樣,等Test方法返回值Task執行完成之後再執行OnExit,統計耗時到返回的Task執行完畢之後:

Test OnEntry
Test start
Test end
Test OnExit - 1096ms

而實際的輸出是下面這樣的,方法的耗時僅為Task對象創建後返回的執行耗時,並沒有等待Task執行:

Test OnEntry
Test start
Test OnExit - 96ms
Test end

這種表現其實是最開始設計時的設定。在我們剛接觸async/await語法時,我們或許有聽到這樣的介紹「async/await讓我們像寫同步方法那樣去寫非同步方法」。是的,有了async/await,我們就不用像以前EAP/APM那樣去編寫callback了,程式碼整體看起來和同步程式碼無異,同時我們也會注意到一點,非同步方法的返回值類型是Task/ValueTask,但是我們實際return的對象類型卻是其泛型參數類型(Task/ValueTask中的那個T),肉夾饃採用了這一設定。所以對於返回值時Task/ValueTask的方法,如果使用了async/await語法,那麼你通過MethodContext.RealReturnType獲取到的返回值類型就是其泛型參數類型(沒有泛型參數時就是void),同時通過MethodContext.HandledExceptionMethodContext.ReplaceReturnValue設置/修改返回值時,返回值的類型也是Task/ValueTask的泛型參數類型。而對於沒有使用async/await語法的方法,那麼返回值類型就是Task/ValueTask本身。也可以簡單的理解為MoAttribute里採用的方法返回值類型與你編寫程式碼時return的對象類型相同。也因為這樣的設定,在上面的示例中由於沒有使用async/await語法,其實際的返回值類型就是Task,並不會去等待Task執行完畢,所以有了上面那段程式碼的執行效果。

如果希望上面那段程式碼達到預期的效果,有沒有什麼方案呢?答案是:有的

// 僅對TimelineAttribute的OnExit方法進行改造
class TimelineAttribute : MoAttribute
{
    // ...

    public override void OnExit(MethodContext context)
    {
        if (typeof(Task).IsAssignableFrom(context.RealReturnType))
        {
            ((Task)context.ReturnValue).ContinueWith(t => _OnExit());
        }
        else
        {
            _OnExit();
        }

        void _OnExit()
        {
            _stopwatch.Stop();
            Console.WriteLine($"{context.Method.Name} {nameof(OnExit)} - {_stopwatch.ElapsedMilliseconds}ms");
        }
    }
}

上面的方案通過自行判斷返回值類型,對繼承自Task的返回值通過顯式轉換後調用ContinueWith達到需要的效果。這個思路是通用的,但對有些需求實現起來就比較麻煩並且需要對肉夾饃的執行邏輯有一定的了解,比如異常處理,上面的例子中如果在Task.Run之前拋出異常,你需要在OnException中進行異常處理,而如果是在Task.Run里的Action中拋出異常,那就需要到OnSuccess中通過ContinueWith判斷Task是否執行異常:

[Timeline]
static Task Test()
{
    Console.WriteLine($"{nameof(Test)} start");
    // throw new Exception(); // 這裡拋出異常在OnException中處理
    return Task.Run(() =>
    {
        // throw new Exception(); // 這裡拋出異常在OnSuccess中通過ContinueWith處理
        Thread.Sleep(1000);
        Console.WriteLine($"{nameof(Test)} end");
    });
}

在兩個issue提出之後,細細想來這種需求或許才是大家最常用的,有時不使用async語法,可能僅僅是因為方法的重載只需對參數稍作處理然後直接調用重載方法即可,這種情況下不使用async語法也是很正常的。針對這類情況,就有了本次版本推出的ExMoAttribute了。

ExMoAttribute

ExMoAttribute的目標是解決前面提到的問題,對沒有使用async語法的方法採用MoAttribute中使用了async語法相同的邏輯。

快速開始

# 添加NuGet引用
dotnet add package Rougamo.Fody
class TimelineAttribute : ExMoAttribute
{
    private Stopwatch _stopwatch;

    protected override void ExOnEntry(MethodContext context)
    {
        _stopwatch = Stopwatch.StartNew();
        Console.WriteLine($"{context.Method.Name} {nameof(OnEntry)}");
    }

    protected override void ExOnExit(MethodContext context)
    {
        _stopwatch.Stop();
        Console.WriteLine($"{context.Method.Name} {nameof(OnExit)} - {_stopwatch.ElapsedMilliseconds}ms");
    }
}

TimelineAttribute改成上面的程式碼即可完成最初統計耗時的需求了,應用了該Attribute的方法無論是否使用async語法都能達到同樣的效果,再也不用去判斷MethodContext.RealReturnType了。

ExMoAttribute的使用差異(重要)

ExMoAttributeMoAttribute除了類名和方法名有所區別之外,在使用時也有些許區別,下面是使用ExMoAttribute時與之前不同的地方:

  • 使用MethodContext.ExReturnValue獲取方法返回值。如果是沒有使用async語法的方法,使用之前的MethodContext.ReturnValue獲取返回值,你獲取到的會是Task/ValueTask類型的返回值,而不是其泛型參數類型;
  • 使用MethodContext.ExReturnType獲取返回值類型。如果你要修改返回值或處理異常,你設置的返回值類型需要與MethodContext.ExReturnType相同;
  • 使用MethodContext.ExReturnValueReplaced獲取返回值是否被修改。如果是沒有使用async語法的方法,使用之前的MethodContext.ReturnValueReplaced獲取到的一直都會是true,因為返回值被ContinueWith返回的Task替換了。

當前版本除了上面三個屬性在使用時需要注意,其他的與MoAttribute基本無異,包括處理異常時依舊調用MethodContext.HandledException方法,修改/設置返回值時依舊調用MethodContext.ReplaceReturnValue方法,後續還會不會有其他差異就需要大家關注一下版本日誌了。