肉夾饃是什麼
肉夾饃通過靜態程式碼織入方式實現AOP的元件。.NET常用的AOP有Castle DynamicProxy、AspectCore等,以上兩種AOP元件都是通過執行時生成一個代理類執行AOP程式碼的,肉夾饃則是在程式碼編譯時直接修改原始方法IL程式碼,在原始方法內織入AOP程式碼的。.NET靜態AOP的元件或許有人使用過PostSharp,這是一個功能完善且強大的靜態程式碼織入元件,Postsharp有社群版,但可惜的是社群版不支援非同步方法,肉夾饃的實現方式與Postsharp類似,同時也支援了非同步方法,如果你僅僅使用了Postsharp方法層級的AOP程式碼織入功能,可以嘗試使用肉夾饃來替代Postsharp。
快速開始
# 新增NuGet引用
dotnet add package Rougamo.Fody
// 1.定義類繼承MoAttribute,在該類中定義你在方法執行各階段需要織入的程式碼
public class LoggingAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
// 從context物件中能取到包括入參、類例項、方法描述等資訊
Log.Info("方法執行前");
}
public override void OnException(MethodContext context)
{
Log.Error("方法執行異常", context.Exception);
}
public override void OnExit(MethodContext context)
{
Log.Info("方法退出時,不論方法執行成功還是異常,都會執行");
}
public override void OnSuccess(MethodContext context)
{
Log.Info("方法執行成功後");
}
}
// 2.在需要織入程式碼的方法上應用LoggingAttribute
public class Service
{
[Logging]
public static int Sync(Model model)
{
// ...
}
[Logging]
private async Task<Data> Async(int id)
{
// ...
}
}
通過實現空介面的方式進行程式碼織入
在上面的示例中,我們通過在方法上應用Attribute進行AOP,這種方式目標明確但有些AOP程式碼我們可能希望應用於某一場景或某一層級,每個方法都去應用Attribute很繁瑣,而且程式碼侵入嚴重。此時就可以考慮使用實現空介面(IRougamo<>
)的方式進行批量Attribute應用
public interface IService : IRougamo<LoggingAttribute> { }
public interface IMyService : IService { }
public class MyService : IMyService
{
}
上面的示例中,MyService
所有的public例項方法都將應用LoggingAttribute
,你可能注意到我標紅的部分了,為什麼是public例項方法呢?這是預設值,你可以在繼承MoAttribute
時通過重寫Flags
屬性來修改這一預設值,比如下面的示例中FullLoggingAttribute
將會應用於所有方法。另外需要注意的是Flags
屬性在Attribute
直接應用到方法上時是無效的,比如LoggingAttribute
預設僅應用public例項方法,但像快速開始裡的程式碼那樣Async
方法雖然是private的但還是會應用LoggingAttribute
public class FullLoggingAttribute : LoggingAttribute
{
public override AccessFlags Flags => AccessFlags.All;
}
例項-Rougamo.OpenTelemetry
在快速開始裡介紹了肉夾饃兩種常用的使用方式,更多的使用方式可以到github檢視readme,在本篇文章中就不再做更多介紹了,接下來我將介紹使用肉夾饃的一個專案Rougamo.OpenTelemetry,如果你準備使用肉夾饃,但你還是不太清楚具體應該怎麼使用,可以參考這個專案的程式碼實現。
關於OpenTelemetry
在瞭解OpenTelemetry
前,你需要先了解APM(Application Performance Management/Monitor)
,在這個微服務的時代,APM已經成為了必不可少的一部分,沒有它整個系統對我們而言就是一個黑盒,你無法得知一個請求在微服務之間是如何呼叫如何完成,難以排查一個使用者超時是哪個服務超時或出錯。現在市面上有很多開源的APM比如Pinpoint, Zipkin, SkyWalking, CAT, jaeger等,雖說大家基本都是參考google的dapper論文設計出來的,但實現和功能側重卻大相徑庭,為了對此形成一個規範,先後出現了OpenTracing
和OpenCensus
,並在此後合併為現在的OpenTelemetry
。OpenTelemetry的出現為APM的接入提供了一種可能“應用不需要在意具體的APM服務端使用的是Zipkin還是jaeger或是其他的情況下,應用只需要使用OpenTelemetry的SDK進行埋點,APM通過實現OTLP(OpenTelemetry Protocol)來支援OpenTelemetry資料格式即可”,當前已經有些APM完全採用OpenTelemetry SDK作為預設的SDK比如jaeger,也有部分支援的APM比如skywalking。
關於Rougamo.OpenTelemetry
現在大部分流行的APM都有對應語言的SDK並且還實現了常用的I/O元件埋點,opentelemetry-dotnet也已經提供了包括HttpClient
、SqlClient
、AspNetCore
等I/O埋點。雖說一般而言服務的耗時一般就在I/O部分,但由於開發人員的程式碼習慣不同、程式碼水平不同以及業務複雜度等情況,某些非I/O程式碼也會產生一定的耗時,同時在一個介面中可能會執行多次I/O操作,如果僅僅只有I/O埋點,可能很難分辨層次關係,此時可能需要一些本地輔助埋點,Rougamo.OpenTelemetry
便是用於新增本地埋點的元件。
快速開始
# 啟動專案引用Rougamo.OpenTelemetry.Hosting
dotnet add package Rougamo.OpenTelemetry.Hosting
# 新增埋點的專案引用Rougamo.OpenTelemetry
dotnet add package Rougamo.OpenTelemetry
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddOpenTelemetryTracing(builder =>
{
builder
.AddRougamoSource() // 初始化Rougamo.OpenTelemetry
.AddAspNetCoreInstrumentation()
.AddJaegerExporter();
});
// 修改Rougamo.OpenTelemetry預設配置
services.AddOpenTelemetryRougamo(options =>
{
options.ArgumentsStoreType = ArgumentsStoreType.Tag;
});
}
}
class Service
{
[return: ApmIgnore] // 返回值不記錄
[Otel] // 預設記錄引數和返回值,需要通過ApmIgnoreAttribute來忽略不需要記錄的引數或返回值
public async Task<string> M1(
[ApmIgnore] string uid, // 該引數不記錄
DateTime time)
{
// do something
return string.Empty;
}
[PureOtel] // 預設不記錄引數和返回值,需要通過ApmRecordAttribute來記錄指定的引數或返回值
public void M2(
[ApmRecord] double d1, // 記錄該引數
double d2)
{
// do something
}
}
// 通過實現空介面織入
public interface ITestService : IRougamo<FullOtelAttribute>
{
// ...
}
public class TestService : ITestService
{
// ...
}
Rougamo.OpenTelemetry
的埋點會對應生成一個名稱為方法全名稱(ClassFullName.MethodName)的LocalSpan
,根據你使用的是OtelAttribute
還是PureOtelAttribute
決定預設是否記錄引數和返回值。Rougamo.OpenTelemetry
是用來豐富APM埋點的,但是切記不要過度新增埋點,過多的埋點會讓你的trace看起來很臃腫。
關於Rougamo.OpenTelemetry更多的使用說明,詳見github,github上的程式碼中包含了一個jaeger的示例程式碼,你可以從jaeger官網上下載一個all-in-one包快速執行一個jaeger服務端,然後啟動示例專案,訪問http://localhost:5000/test
介面,最後訪問jaeger uihttp://localhost:16686
檢視剛剛訪問的test介面的trace資料。
更多關於
關於肉夾饃的應用情況
寫肉夾饃的動機是公司在使用postsharp
做AOP,起初公司的程式碼是framework的並且基本使用同步方法,所以postsharp的免費版本是足足夠用的,隨著.NET的發展,公司的程式碼也逐漸從同步發展到非同步從framework發展到core,然後我們通過購買付費版本的postsharp也能繼續維持著,不過由於個人對postsharp的實現產生了興趣,所以悄悄的建立了這個專案,但是由於個人比較懶,這個早在19年就建立了的專案直到21年才完成。
在釋出1.0.1之前,專案一直處於閉源狀態,但在閉源狀態下已經在公司內部發布了幾個測試版本,其中1.0.0版本已經在公司測試環境沉澱了一個季度有餘,現在已經將1.0.0版本釋出到了線上使用中,釋出在nuget.org上的1.0.1版本相對於1.0.0版本在程式碼上沒有任何修改。Rougamo
專案的TargetFramework是netstandard2.0
,公司應用了Rougamo
的專案都是.NET Core3.1的,所以如果你的專案是.NET Core3.1的,你可以相對放心的使用(如果不著急應用,也推薦測試環境沉澱一下),如果你是其他版本,那麼推薦你在測試環境沉澱一段時間,肉夾饃作為一個新專案,可能還會存在一些未知BUG,如果有任何BUG請反饋到github issue中。
關於.NET的靜態程式碼織入
.NET的靜態程式碼織入其實我瞭解的也不是特別多,我知道鼻祖應該是Mono.Cecil
,百度也能搜到很多它的介紹,然後就是很強大(但大部分功能收費)的Postsharp
,以及對Mono.Cecil
進行封裝,使其更易用的Fody,肉夾饃便是使用Fody
實現AOP程式碼織入的。
靜態程式碼織入在我觀察下來使用得並不是很普遍,這或許是因為動態代理早已成熟的緣故吧。那麼靜態織入相對於動態代理有什麼優勢呢?說實話,開發肉夾饃很大一部分原因是個人興趣,但這並不代表它沒有優勢,靜態織入是在編譯時進行的,靜態織入只會讓編譯時間稍長些許,而動態代理的方式都是在應用啟動時動態生成代理類來實現的,這個過程必定會佔用些許時間,並且在這個初始化動作完成前,服務是不會進入就緒狀態的,也就是這個服務暫時為不可用狀態的,服務初始化時間越短,服務整體的可用性就會越好,這就是靜態織入帶來的優勢。當然,有些朋友可能會認為這是在鑽牛角尖,確實,很多時候我們可能認為這種耗時是微乎其微的,事實也確實如此,但做基礎架構關注的就是這些微乎其微耗時,我們經常能看到java的一些技術博文上會寫到他們做了很多位元組碼層面的優化,他們的這種優化很多時候只是優化了那麼幾個指令,單拎出來看著似乎沒有多大的效能提升,然而在大流量高吞吐的服務中,這樣優化的效果將會顯現出來,靜態織入也是如此,效能就是這樣一點一點扣出來的。
關於Fody
.NET的開發者應該或多或少都聽說甚至使用過ABP,它是.NET中非常流行的一套DDD框架了,如果你還看過ABP的原始碼,你或許見過Fody的影子,是的ABP也有使用到Fody,使用的是ConfigureAwait.Fody,我們在編寫非同步方法的時候經常會增加一個.ConfigureAwait(false)
,ConfigureAwait.Fody
的功能就是為非同步呼叫預設加上這個方法呼叫。
進入到Fody的github首頁你將能看到很多借助於Fody開發的元件,我們也可以直接在nuget.org上以Fody為關鍵字進行搜尋,你將能看到更多以Fody開發的元件,同時你可能還會發現,在下載量很高的NuGet包中有兩個AOP相關實現MethodDecorator.Fody和MethodBoundaryAspect.Fody,早在我建立肉夾饃這個專案前我就看到了這兩個專案,但當時的他們沒有對非同步方法的支援,就在這篇文章寫到這裡的時候我再次去檢視了這兩個專案,他們對非同步的支援依舊不能滿足我的需求,他們的OnExit
方法都是在狀態機在第一次返回也就是在遇到第一個await的時候執行的,這時候這個非同步方法實際上可能並沒有執行完畢,下面我會給一個例子,各位可以自己進行嘗試。關於為什麼我沒有直接參與他們的專案,而是自己新建了一個專案,主要有兩個原因:一是我有一丟丟懶,不確定這個專案我會投入多少精力並且什麼時候去完成,事實也正如我的預期,兩年過去了,二是我的英語有一丟丟差,IL方面我也不算老手,我擔心有些問題交流起來有困難,所以最終也就獨立建了肉夾饃這個專案了。
dotnet add package Rougamo.Fody
dotnet add package MethodDecorator.Fody
dotnet add package MethodBoundaryAspect.Fody
<!--FodyWeavers.xml-->
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo />
<MethodDecorator />
<MethodBoundaryAspect />
</Weavers>
public sealed class RougamoLogAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
Console.WriteLine($"[Rougamo] on entry");
}
public override void OnSuccess(MethodContext context)
{
Console.WriteLine($"[Rougamo] on success");
}
public override void OnException(MethodContext context)
{
Console.WriteLine($"[Rougamo] on exception: {context.Exception.Message}");
}
public override void OnExit(MethodContext context)
{
Console.WriteLine($"[Rougamo] on exit");
}
}
[AttributeUsage(AttributeTargets.Method)]
public class MethodDecoratorLogAttribute : Attribute, IMethodDecorator
{
public void Init(object instance, MethodBase method, object[] args)
{
Console.WriteLine($"[MethodDecorator] on init");
}
public void OnEntry()
{
Console.WriteLine($"[MethodDecorator] on entry");
}
public void OnExit()
{
Console.WriteLine($"[MethodDecorator] on exit");
}
public void OnException(Exception exception)
{
Console.WriteLine($"[MethodDecorator] on exception: {exception.Message}");
}
}
public sealed class MethodBoundaryAspectLogAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Console.WriteLine($"[MethodBoundaryAspect] on entry");
}
public override void OnExit(MethodExecutionArgs args)
{
Console.WriteLine($"[MethodBoundaryAspect] on exit");
}
public override void OnException(MethodExecutionArgs args)
{
Console.WriteLine($"[MethodBoundaryAspect] on exception: {args.Exception.Message}");
}
}
class Program
{
static async Task Main(string[] args)
{
try
{
await Async();
}
catch
{
}
}
[RougamoLog]
//[MethodDecoratorLog]
//[MethodBoundaryAspectLog]
static async Task Async()
{
Console.WriteLine(1);
await Task.Delay(10);
Console.WriteLine(2);
throw new NotImplementedException("not implemented");
}
}
分別用三個Attribute執行上面的程式你會得到下面的輸出,肉夾饃的異常資訊是在輸出2之後輸出,exit資訊在最後輸出(也就是非同步方法執行完畢後);MethodDecorator沒有捕獲到非同步的異常,並且exit資訊在輸出2之前就輸出了;MethodBoundaryAspect捕獲到了非同步的異常資訊,但是exit資訊在輸出2之前輸出了,也就是你無法在非同步方法真正執行完畢後織入程式碼。
[Rougamo] on entry
1
2
[Rougamo] on exception: not implemented
[Rougamo] on exit
[MethodDecorator] on init
[MethodDecorator] on entry
1
[MethodDecorator] on exit
2
[MethodBoundaryAspect] on entry
1
[MethodBoundaryAspect] on exit
2
[MethodBoundaryAspect] on exception: not implemented
關於使用肉夾饃開發元件的注意事項
最後如果你準備使用肉夾饃,並且你準備使用肉夾饃開發一個供他人使用的NuGet元件,那麼你需要把專案檔案(.csproj)中Rougamo.Fody
的引用改成下面這樣,不然你釋出的NuGet其他人引用後將需要額外引用Fody,否則將無法進行程式碼織入,具體可以參考Rougamo.OpenTelemetry
<PackageReference Include="Rougamo.Fody" Version="1.0.1" IncludeAssets="all" PrivateAssets="contentfiles;analyzers" />
最後的最後,即使你不準備使用肉夾饃,也希望通過此文讓你瞭解到靜態程式碼織入,瞭解到Mono.Cecil
和Fody
,如果.NET能夠發展壯大起來,那麼靜態程式碼織入也終將得到更大的發展。這篇文章中不論是Rougamo還是Rougamo.OpenTelemetry都沒有進行完整的介紹,如果你準備使用它們,請移步github瞭解更多。