.NET静态代码AOP——肉夹馍(Rougamo)
肉夹馍是什么
肉夹馍(https://github.com/inversionhourglass/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.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了解更多。
2024-01-22 15:32:47【出处】:https://www.cnblogs.com/nigture/p/15712899.html
=======================================================================================
关注我】。(●'◡'●)
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的【因为,我的写作热情也离不开您的肯定与支持,感谢您的阅读,我是【Jack_孟】!
本文来自博客园,作者:jack_Meng,转载请注明原文链接:https://www.cnblogs.com/mq0036/p/17980169
【免责声明】本文来自源于网络,如涉及版权或侵权问题,请及时联系我们,我们将第一时间删除或更改!