.NET静态代码织入——肉夹馍(Rougamo)

时间:2022-12-27 23:05:49

肉夹馍是什么

肉夹馍通过静态代码织入方式实现AOP的组件。.NET常用的AOP有Castle DynamicProxyAspectCore等,以上两种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论文设计出来的,但实现和功能侧重却大相径庭,为了对此形成一个规范,先后出现了OpenTracingOpenCensus,并在此后合并为现在的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也已经提供了包括HttpClientSqlClientAspNetCore等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.FodyMethodBoundaryAspect.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>
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.CecilFody,如果.NET能够发展壮大起来,那么静态代码织入也终将得到更大的发展。这篇文章中不论是Rougamo还是Rougamo.OpenTelemetry都没有进行完整的介绍,如果你准备使用它们,请移步github了解更多。

.NET静态代码织入——肉夹馍(Rougamo)的更多相关文章

  1. 30个类手写Spring核心原理之AOP代码织入(5)

    本文节选自<Spring 5核心原理> 前面我们已经完成了Spring IoC.DI.MVC三大核心模块的功能,并保证了功能可用.接下来要完成Spring的另一个核心模块-AOP,这也是最 ...

  2. Spring的LoadTimeWeaver&lpar;代码织入&rpar;

    在Java 语言中,从织入切面的方式上来看,存在三种织入方式:编译期织入.类加载期织入和运行期织入.编译期织入是指在Java编译期,采用特殊的编译器,将切面织入到Java类中:而类加载期织入则指通过特 ...

  3. Spring的LoadTimeWeaver&lpar;代码织入&rpar;&lpar;转&rpar;

    https://www.cnblogs.com/wade-luffy/p/6073702.html 在Java 语言中,从织入切面的方式上来看,存在三种织入方式:编译期织入.类加载期织入和运行期织入. ...

  4. 【开源】&period;Net Aop&lpar;静态织入&rpar;框架 BSF&period;Aop

    BSF.Aop .Net 免费开源,静态Aop织入(直接修改IL中间语言)框架,类似PostSharp(收费): 实现前后Aop切面和INotifyPropertyChanged注入方式. 开源地址: ...

  5. Java AOP &lpar;1&rpar; compile time weaving 【Java 切面编程 &lpar;1&rpar; 编译期织入】

    According to wikipedia  aspect-oriented programming (AOP) is a programming paradigm that aims to inc ...

  6. AOP静态代理解析2-代码织入

    当我们完成了所有的AspectJ的准备工作后便可以进行织入分析了,首先还是从LoadTimeWeaverAwareProcessor开始. LoadTimeWeaverAwareProcessor实现 ...

  7. Spring AOP 之编译期织入、装载期织入、运行时织入&lpar;转&rpar;

    https://blog.csdn.net/wenbingoon/article/details/22888619 一   前言 AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP ...

  8. AOP 动态织入的&period;NET实现

    AOP(面向切面编程:Aspect Oriented Programming)为诸如日志记录.性能统计.安全控制.事务处理.异常处理等与具体业务逻辑无关,却需要在全局范围进行执行的功能提供了一种良好重 ...

  9. Spring配置AOP实现定义切入点和织入增强

    XML里的id=””记得全小写 经过AOP的配置后,可以切入日志功能.访问切入.事务管理.性能监测等功能. 首先实现这个织入增强需要的jar包,除了常用的 com.springsource.org.a ...

随机推荐

  1. EXD&lowbar;BAD&lowbar;ACCEEE

    iOS开发过程中,普通的bug通常较容易定位问题所在,但是,EXD_BAD_ACCEEE问题却比较不易查找问题.本文记录下解决EXD_BAD_ACCEEE问题的过程.首先说一下 EXC_BAD_ACC ...

  2. 使用JavaScript访问子节点方法elementNode&period;childNodes时,需要注意的地方

    有这样一个HTML结构 <div> javascript <p>javascript</p> <div>jQuery</div> <h ...

  3. CentOS安装zip unzip命令

    yum install zip unzip

  4. choop&period;php一句话脚本

    <?php$_="";$_[+$_]++;$_=$_.""; $___=$_[+""];//A$____=$___;$____++;/ ...

  5. SQL Server 存储过程(转)

    Transact-SQL中的存储过程,非常类似于Java语言中的方法,它可以重复调用.当存储过程执行一次后,可以将语句缓存中,这样下次执行的时候直接使用缓存中的语句.这样就可以提高存储过程的性能. Ø ...

  6. Java多线程2:实现多线程的两种方式

    原文:http://www.cnblogs.com/skywang12345/p/3479063.html 常见的实现多线程的方式有2种,一是继承Thread类,二是实现 Runnable接口,还可以 ...

  7. spfa&lowbar;dfs找负环

    luogu #include<iostream> #include<cstdio> #include<cstring> #include<vector> ...

  8. 2018&period;5&period;11 B树总结

    小结 B树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于 走右结点: B-树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键 字范围的子结点: 所有关键字在整颗 ...

  9. PDF&period;js 分片下载的介绍2&colon;分片下载demo

    上一个章节,简要说了以下分片下载的几个特性.今天主要用示例说明一下pdf.js分片下载. 服务器环境: php7.2 nginx 1.14 ubuntu 18.04测试浏览器:谷歌浏览器 70.0.3 ...

  10. C&num;编程(十七)----------Object类

    Object类 它是.NET Framework 中所有类的最终基类:它是类型层次结构的根.也就是说所有的类都拥有object类的方法,并能重写,调用. object的构造函数:public Obje ...