肉夾饃(https://github.com/inversionhourglass/Rougamo)透過靜態程式碼織入方式實現AOP的元件,其主要特點是在編譯時完成AOP程式碼織入,相比動態代理可以減少應用啟動的初始化時間讓服務更快可用,同時還能對靜態方法進行AOP操作。
正文
雖又是一個大版本,但本次大版本沒有重大的功能上線,主要是修改了程式碼織入方式,這樣的改動牽扯到一些現有功能。
程式碼織入方式變化
在3.0之前的版本中採用的是程式碼內嵌的方式進行織入,下面用簡化程式碼進行演示:
// 原始方法
public void M()
{
Console.WriteLine(1);
Console.WriteLine(2);
Console.WriteLine(3);
}
// 3.0版本之前織入程式碼後
public void M()
{
var context = new MethodContext(...);
var mo = new AbcAttribute();
mo.OnEntry(context);
try
{
Console.WriteLine(1);
Console.WriteLine(2);
Console.WriteLine(3);
mo.OnSuccess(context);
}
catch (Exception e)
{
context.Exception = e;
mo.OnException(context);
throw;
}
finally
{
mo.OnExit(context);
}
}
在3.0版本中採用的是代理呼叫的方式進行程式碼織入,將原方法複製為一個新方法$Rougamo_M
,然後修改原方法進行程式碼織入後呼叫$Rougamo_M
,簡化程式碼如下:
// 將原方法M複製為$Rougamo_M
public void $Rougamo_M()
{
Console.WriteLine(1);
Console.WriteLine(2);
Console.WriteLine(3);
}
// 修改原方法進行程式碼織入並呼叫複製後的原方法
public void M()
{
var context = new MethodContext(...);
var mo = new AbcAttribute();
mo.OnEntry(context);
try
{
$Rougamo_M();
mo.OnSuccess(context);
}
catch (Exception e)
{
context.Exception = e;
mo.OnException(context);
throw;
}
finally
{
mo.OnExit(context);
}
}
不同織入方法帶來的影響
ExMoAttribute的棄用
早在1.2版本中新增了ExMoAttribute
,可能很多朋友對此都並不瞭解,ExMoAttribute
主要用來遮蔽使用和不使用async/await語法所帶來的差異,因為使用async/await語法後,在編譯時會生成對應的狀態機型別,那麼肉夾饃就會對應修改狀態機程式碼進行織入,而不使用async/await語法的方法就只能對原方法進行程式碼織入,下面用程式碼簡單演示其中的差異:
public async Task Delay()
{
Console.WriteLine(1);
await Task.Delay(2000);
Console.WriteLine(2);
}
// 使用async/await語法呼叫Delay
public async Task WithSyntax()
{
var context = new MethodContext(...);
var mo = new AbcAttribute();
mo.OnEntry(context);
try
{
await Delay();
mo.OnSuccess(context);
}
catch (Exception e)
{
context.Exception = e;
mo.OnException(context);
throw;
}
finally
{
mo.OnExit(context);
}
}
// 不使用async/await語法呼叫Delay
public Task WithoutSyntax()
{
var context = new MethodContext(...);
var mo = new AbcAttribute();
mo.OnEntry(context);
try
{
var task = Delay();
mo.OnSuccess(context);
return task;
}
catch (Exception e)
{
context.Exception = e;
mo.OnException(context);
throw;
}
finally
{
mo.OnExit(context);
}
}
在上面的程式碼示例中,沒有使用async/await的WithoutSyntax
方法會在Delay
還沒有執行完畢之前就執行OnSuccess
和OnExit
方法。
ExMoAttribute
針對沒有使用async/await語法的方法透過在OnSuccess
方法中使用ContinueWith
達到在非同步方法實際執行完畢後執行OnExit
系列方法。ExMoAttribute
雖然能夠解決語法差異帶來的問題,但也增加了一定的複雜性,同時因為其可能鮮為人知,所以在使用時因語法差異帶來的問題可能後知後覺。
在3.0版本中由於使用代理呼叫的方式,被代理方法是否使用async/await語法是被遮蔽的,代理方只需要知道你的返回值是Task
即可,所以在3.0版本中MoAttribute
即可應對是否使用async/await語法的兩種情況,ExMoAttribute
在3.0版本中標記為Obsolete並將在4.0版本中直接刪除。下面用程式碼簡單說明3.0對於是否使用async/await語法的統一處理方式:
// 複製WithSyntax原方法為$Rougamo_WithSyntax
public async Task $Rougamo_WithSyntax()
{
await Delay();
}
// 複製WithoutSyntax原方法為$Rougamo_WithoutSyntax
public Task $Rougamo_WithoutSyntax()
{
return Delay();
}
public async Task WithSyntax()
{
// ...程式碼織入
try
{
await $Rougamo_WithSyntax();
}
catch
{
// ...程式碼織入
}
}
public async Task WithoutSyntax()
{
// ...程式碼織入
try
{
await $Rougamo_WithoutSyntax();
}
catch
{
// ...程式碼織入
}
}
async void 弱支援
如果說前面介紹的是織入方式改變帶來的優勢,那麼這裡介紹的就是劣勢了。async void方法是一種特殊的非同步方法,同樣會生成對應的狀態機型別,但呼叫該方法無法進行await操作,也就無法等待該方法實際執行完畢。在3.0版本之前,由於採取的是內嵌程式碼織入,直接修改狀態機程式碼完成織入,所以OnExit
系列方法可以在正確的時間點執行,而在3.0版本後由於採用了代理呼叫的方式,所以在執行OnExit
系列方法時無法確保方法實際已經執行完畢。
關於async void的織入方式目前還在思考中,考慮到winform和wpf中可能存在不少的async void寫法,代理呼叫的織入方式可能就無法滿足目前的使用要求了,所以我將在github中釋出一個issue進行投票統計,請日常開發涉及到async void的朋友移步到github( https://github.com/inversionhourglass/Rougamo/issues/68 )中進行投票,投票將在4.0版本開發末期截止。
支援步入除錯
在3.0版本之前,應用了肉夾饃完成織入的方法在開發時無法進行步入除錯,這是因為之前的版本沒有對除錯資訊做對應的修改,沒有去做這一功能也是因為比較複雜懶得整。在3.0修改程式碼織入方式後,修改對應的除錯資訊相對要簡單許多,因此3.0版本支援步入除錯
僅ref/out支援重新整理引數
在2.1版本中新增重新整理引數功能,支援在OnSuccess / OnException / OnExit
中透過MethodContext.Arguments
獲取最新的引數值,但在3.0版本之後,由於織入程式碼方式的改變,此功能僅支援ref
和out
引數。
public void M(int x, out decimal y, ref string z)
{
// ...
}
// 3.0 版本之前的織入方式
public void M(int x, out decimal y, ref string z)
{
try
{
// ...
// 由於是內嵌織入,在這裡可以直接獲取到所有最新的引數值,所以引數x也可以更新
context.Arguments[0] = x;
context.Arguments[1] = y;
context.Arguments[2] = z;
mo.OnSuccess(context);
}
catch (Exception e)
{
// ...
// 由於是內嵌織入,在這裡可以直接獲取到所有最新的引數值,所以引數x也可以更新
context.Arguments[0] = x;
context.Arguments[1] = y;
context.Arguments[2] = z;
mo.OnException(context);
throw;
}
}
// 3.0 版本的織入方式
public void M(int x, out decimal y, ref string z)
{
try
{
$Rougamo_M(x, out y, ref z);
// 由於是代理呼叫織入,引數x在$Rougamo_M中被重新賦值後無法在外部獲取,所以僅更新引數y和z
context.Arguments[1] = y;
context.Arguments[2] = z;
mo.OnSuccess(context);
}
catch (Exception e)
{
// ...
}
}
構造方法織入方式不變
由於構造方法較為特殊,readonly欄位僅可在構造方法中初始化,所以無法使用代理呼叫的織入方式,這也表示使用肉夾饃程式碼織入的構造方法無法支援步入除錯。
織入方式切換
新的編織方式涉及眾多程式碼,程式碼織入部分的程式碼近乎重寫,雖然做了大量的測試,但為了保證穩定性提供了降級配置,修改專案中FodyWeavers.xml
檔案中Rougamo
節點配置,透過設定proxy-calling="false"
,將織入方式改回3.0版本之前的內嵌織入方式。需要注意的是,該配置僅為過渡配置,將在4.0版本中移出並最終僅保留代理織入的方式,如果代理織入的方式存在任何問題,請及時反饋。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo proxy-calling="false" />
</Weavers>
其他更新
以下列出3.0版本相關的所有issue,有興趣的可以直接移步github檢視issue中的回覆
- #36 應用Rougamo的方法支援步入除錯
- #54 解決snupkg報checksum錯誤的問題,需直接依賴Fody,詳見issue回覆
- #60 支援自定義AsyncMethodBuilder
- #63 支援泛型Attribute
- #65 修復特定Type型別無法作為MoAttribute構造方法引數