5

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

 2 years ago
source link: https://www.cnblogs.com/nigture/p/15712899.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

肉夹馍是什么

肉夹馍通过静态代码织入方式实现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了解更多。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK