.NET静态代码织入——肉夹馍(Rougamo)
肉夾饃是什么
肉夾饃通過靜態代碼織入方式實現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.OpenTelemetrypublic 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 somethingreturn 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方面我也不算老手,我擔心有些問題交流起來有困難,所以最終也就獨立建了肉夾饃這個項目了。
分別用三個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了解更多。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的.NET静态代码织入——肉夹馍(Rougamo)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NET 6 中新增的LINQ 方法
- 下一篇: Asp.NetCore3.1开源项目升级