首页 > 其他分享 >.NET静态代码织入——肉夹馍(Rougamo) 发布1.4.0

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

时间:2023-03-04 12:46:16浏览次数:50  
标签:1.4 Retry Rougamo 肉夹馍 void 重试 context public

肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应用启动的初始化时间让服务更快可用,同时还能对静态方法进行AOP。

距离上一次发文差不多过去半年了,在这半年中其实还发布了一个1.3.0版本,不过因为感觉几句话就介绍完了,所以上次也就没有发文了,这次也会先简短的介绍1.3.0中新增的功能。

重写方法参数(v1.3.0)

1.3.0版本新增的功能支持我们在OnEntry中修改方法的参数值,通过这个功能,我们可以完成复杂的参数默认值设置,甚至可以通过该功能实现方法注入。

// 下面的例子是丰富日志内容,每次输出日志时输出时间前缀
class EnrichMessageAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        if (context.Arguments.Length == 1 && context.Arguments[0] is string message)
        {
            context.RewriteArguments = true;
            context.Arguments[0] = $"[{DateTime.Now}] {message}";
        }
    }
}

[EnrichMessage]
static void Log(string message)
{
    Console.WriteLine(message);
}

Log("test"); // [2023-3-3 00:03:17] test

MoAttributeExMoAttribute都支持重写参数,需要注意的是,重写参数时需要将MethodContext.RewriteArguments设置为true,仅仅修改context.Arguments元素是不生效的。

重试(v1.4.0)

1.4.0版本新增的功能可以让我们在遇到指定异常或者返回值非预期值的情况下重新执行当前方法,实现方式是在OnExceptionOnSuccess中设置MethodContext.RetryCount值,在OnExceptionOnSuccess执行完毕后如果MethodContext.RetryCount值大于0那么就会重新执行当前方法。

internal class RetryAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        context.RetryCount = 3;
    }

    public override void OnException(MethodContext context)
    {
        context.RetryCount--;
    }

    public override void OnSuccess(MethodContext context)
    {
        context.RetryCount = 0;
    }
}

// 应用RetryAttribute后,Test方法将会重试3次
[Retry]
public void Test()
{
    throw new Exception();
}

使用重试功能需要注意以下几点:

  • 在通过MethodContext.HandledException()处理异常或通过MethodContext.ReplaceReturnValue()修改返回值时会直接将MethodContext.RetryCount置为0,因为手动处理异常和修改返回值就表示你已经决定了该方法的最终结果,所以就不再需要重试了
  • OnEntryOnExit只会执行一次,不会因为重试而多次执行,OnExceptionOnSuccess根据你设置的RetryCount可能会执行多次
  • 尽量不要在ExMoAttribute中使用重试功能,除非你真的知道实际的处理逻辑。如果对ExMoAttribute不太了解,可以回顾一下之前1.2.0版本发布的 .NET静态代码织入——肉夹馍(Rougamo) 发布1.2.0
    ExMoAttribute能够让我们无区别的对待使用和不使用async/await语法糖的Task/ValueTask返回值方法,主要应用于下面这种没有使用async/await语法糖的场景。那么为什么这种情况下推荐使用重试功能呢?我们看看下面这段代码,可以简单思考一下。
    public Task Test()
    {
      DoSomething();
    
      return Task.Run(() => DoOtherThings());
    }
    
    解释这个问题,首先就要知道肉夹馍的大概工作方式,肉夹馍是静态代码织入,是直接修改当前方法的IL代码来增加AOP功能的,重试功能也就相当于是用一个try..catch..将Test方法内部的代码包裹。
    那么想想看,如果执行DoSomething抛出了异常,那么try..catch..很容的能抓取到这个异常并进行重试,但如果是DoOtherThings抛出异常呢,虽然说我们还是有方法能够获取到这个异常(ExMoAttribute就这么做了),但我们却无法在Task内部出现异常后重新执行Task外面包括DoSomething的那部分代码。
    所以尽量不要在ExMoAttribute中使用重试功能,除非你真的知道实际的处理逻辑,并且认为这个处理逻辑是满足你的需求的。

Rougamo.Retry

在实现重试这个功能的时候我想到,这个功能最常用的场景也就是遇到异常进行重试了,所以在完成该功能的同时我新建了Rougamo.Retry这个项目( https://github.com/inversionhourglass/Rougamo.Retry ),该项目封装出了RetryAttributeRecordRetryAttribute两个Attribute,我们可以简单通过在方法上增加一个Attribute让它在抛异常时重新执行。

快速开始

// 执行M1Async抛出任何异常都将重试一次
[Retry]
public async Task M1Async()
{
}

// 执行M2抛出任何异常都将重试,最多重试三次
[Retry(3)]
public void M2()
{
}

// 执行M3Async抛出IOException或TimeoutException时将重试,最多重试五次
[Retry(5, typeof(IOException), typeof(TimeoutException))]
public static async ValueTask M3Async()
{
}

// 如果异常匹配逻辑复杂,可自定义类型实现IExceptionMatcher
class ExceptionMatcher : IExceptionMatcher
{
    public bool Match(Exception e) => true;
}
[Retry(2, typeof(ExceptionMatcher))]
public static void M4()
{
}

// 如果重试的次数也是固定的,可自定义类型实现IRetryDefinition
class RetryDefinition : IRetryDefinition
{
    public int Times => 3;

    public bool Match(Exception e) => true;
}
[Retry(typeof(RetryDefinition))]
public void M5()
{
}

记录异常

有时候我们可能还希望在遇到异常时,重试的同时能够将异常信息记录到日志中,此时便可以实现IRecordable系列接口了

// 实现IRecordableMatcher接口将不包含重试次数定义
class RecordableMatcher : IRecordableMatcher
{
    public bool Match(Exception e) => true;

    public void TemporaryFailed(ExceptionContext context)
    {
        // 当前方法还有重试次数
        // 可通过context.Exception获取到当前异常
    }

    public void UltimatelyFailed(ExceptionContext context)
    {
        // 当前方法重试次数已用完,最终还是执行失败了
        // 可通过context.Exception获取到当前异常
    }
}
[Retry(3, typeof(RecordableMatcher))]
public async ValueTask M6Async()
{
}

// 实现IRecordableRetryDefinition接口将包含重试次数定义
class RecordableRetryDefinition : IRecordableRetryDefinition
{
    public int Times => 3;

    public bool Match(Exception e) => true;

    public void TemporaryFailed(ExceptionContext context)
    {
        // 当前方法还有重试次数
        // 可通过context.Exception获取到当前异常
    }

    public void UltimatelyFailed(ExceptionContext context)
    {
        // 当前方法重试次数已用完,最终还是执行失败了
        // 可通过context.Exception获取到当前异常
    }
}
[Retry(typeof(RecordableRetryDefinition))]
public async Task M7Async()
{
}

依赖注入

记录异常的方式有很多种,比较常用的应该就是写入日志了,而很多日志框架都是需要依赖注入支持的,而Rougamo.Retry本身是没有依赖注入功能的,上面定义的类型都将使用无参构造方法创建其对象。
考虑到依赖注入的普遍性,所以增加了两个扩展项目Rougamo.Retry.AspNetCoreRougamo.Retry.GeneralHost

Rougamo.Retry.AspNetCore

// 1. 定义实现IRecordableMatcher或IRecordableRetryDefinition的类型,并注入和使用ILogger
class RecordableRetryDefinition : IRecordableRetryDefinition
{
    private readonly ILogger _logger;

    public RecordableRetryDefinition(ILogger<RecordableRetryDefinition> logger)
    {
        _logger = logger;
    }

    public int Times => 3;

    public bool Match(Exception e) => true;

    public void TemporaryFailed(ExceptionContext context)
    {
        // 当前方法还有重试次数
        _logger.LogDebug(context.Exception, string.Empty);
    }

    public void UltimatelyFailed(ExceptionContext context)
    {
        // 当前方法重试次数已用完,最终还是执行失败了
        _logger.LogError(context.Exception, string.Empty);
    }
}

// 2. 在Startup中进行初始化
class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 2.1. 将获取对象的工厂改为IServiceProvider
        services.AddAspNetRetryFactory();
        // 2.2. 注册RecordableRetryDefinition
        services.AddTransient<RecordableRetryDefinition>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 2.3. 注册相关的Middleware,尽量放到前面,否则如果前面的Middleware有用到Rougamo.Retry可能会出现异常
        app.UseRetryFactory();
    }
}

// 3. 使用,使用还是和之前一样,但是这里的RecordableRetryDefinition就可以使用依赖注入了
[Retry(typeof(RecordableRetryDefinition))]
public static async Task M8Async()
{
}

Rougamo.Retry.GeneralHost

除了AspNetCore,我们可能还会创建一些通用程序,此时就可以引用Rougamo.Retry.GeneralHost了,Rougamo.Retry.GeneralHost的初始化比Rougamo.Retry.AspNetCore简单一些,不需要注册Middleware

// 使用部分相同,这里省略

// 在ConfigureServices中进行初始化
public void ConfigureServices(IServiceCollection services)
{
    // 1. 将获取对象的工厂改为IServiceProvider
    services.AddRetryFactory();
    // 2. 注册RecordableMatcher
    services.AddTransient<RecordableMatcher>();
}

AspNetCore中之所以要注册Middleware,原因是AspNetCore中一般有一些类型会注册为Scoped生命周期,所以AspNetCore中会额外注册一个Middleware处理IServiceProvider的获取逻辑,如果你的通用程序中也存在Scoped生命周期,并且实现Rougamo.Retry的相关接口时注入了Scoped类型,那么你可以参考Rougamo.Retry.AspNetCore也进行一些额外的处理

统一记录异常

如果记录异常的逻辑是通用的,那么每次实现IRecordableMatcherIRecordableRetryDefinition接口时都要带上这段逻辑处理会有些麻烦,虽然说可以抽象父类,但还是会稍显麻烦而且有遗漏的可能。
考虑到这个问题Rougamo.Retry也提供了统一记录异常的方式,那就是RecordRetryAttributeIRecordable的组合。

// 1. 实现IRecordable接口
class Recordable : IRecordable
{
    private readonly ILogger _logger;

    public Recordable(ILogger<Recordable> logger)
    {
        _logger = logger;
    }

    public void TemporaryFailed(ExceptionContext context)
    {
        // 当前方法还有重试次数
        _logger.LogDebug(context.Exception, string.Empty);
    }

    public void UltimatelyFailed(ExceptionContext context)
    {
        // 当前方法重试次数已用完,最终还是执行失败了
        _logger.LogError(context.Exception, string.Empty);
    }
}

// 2. 注册Recordable,注意这里只展示了额外的步骤,如果你使用了Rougamo.Retry.AspNetCore或Rougamo.Retry.GeneralHost,那么你同样需要完成这些组件各自的初始化操作
public void ConfigureServices(IServiceCollection services)
{
    services.AddRecordable<Recordable>();
}

// 3. 使用,以下操作都会自动执行Recordable的异常记录动作
[RecordRetry]
public async Task M10Async() { }

[RecordRetry(5, typeof(IOException), typeof(TimeoutException))]
public static async ValueTask M12Async() { }

class ExceptionMatcher : IExceptionMatcher
{
    public bool Match(Exception e) => true;
}
[RecordRetry(2, typeof(ExceptionMatcher))]
public static void M13() { }
C# 复制 全屏

Rougamo.Retry注意事项

  • 在使用RetryAttributeRecordRetryAttribute时,当前项目必须直接引用Rougamo.Retry,不可间接引用,否则代码无法织入。
  • Rougamo.Retry主要使用的是重试功能,同时RetryAttributeRecordRetryAttribute继承自MoAttribute而不是ExMoAttribute,所以同样不推荐将这些Attribute应用到没有使用async/await语法糖的方法上

最后

感谢大家的使用和反馈,我最开始想到肉夹馍可以完成的AOP功能基本都实现了,没错,其实大部分功能都是在项目建立之初就想好了,只是因为比较懒,所以这1.0版本到现在1.4版本拖了一年多。
当然也不是说肉夹馍的功能就开发至此不再更新了,其实还有一个功能是计划中的,不过这个功能属于增强不属于前面那种全新的功能。如果大家有想到什么肉夹馍能做的功能,也可以到github上反馈,不过新功能的实现可能就比较拖沓了..
特别感谢在github上反馈问题的朋友们,也因为比较懒,所以测试用例的覆盖面也不是很全,好些BUG都是靠大家反馈的。1.4.0版本现已出发,那么BUG的探索就交给大家了,就算是懒,有BUG还是会尽快修复的。

 

原理上是在MSBuild上增加一个Task,如果不想纠结这个MSBuild,可以直接像我这样引用Fody,它会帮我们做这部分工作,我们主要处理IL部分就可以了,你可以试试。https://github.com/Fody/Fody

标签:1.4,Retry,Rougamo,肉夹馍,void,重试,context,public
From: https://www.cnblogs.com/chinasoft/p/17178079.html

相关文章

  • 1.4 算法和算法分析
    1.4算法和算法分析算法定义对特定问题求解方法和步骤的一种描述,它是指令的有限序列。其中每个指令的表示一个或多个操作。简而言之,算法就是解决问题的方法和步骤。......
  • 1.4 “道家”思想的核心与老子的论道
    道不是这个,也不是那个老子是道家学说的奠基人,一部《道德经》,是道家的最重要的三部著作之一。老子的核心观念就是一个“道”字。“道”字如何把它译成欧洲语言?最初的阶段......
  • SmallDesktopDisplay V1.4.3 学习记录 程序基本流程
    SmallDesktopDisplayV1.4.3学习记录声明:原作者:Misaka;修改:微车游;再次修改:丘山鹤项目地址:https://github.com/SmallDesktopDisplay-team/SmallDesktopDisplay本文引用......
  • Solon v2.1.4 发布。支持 java、kotlin、groovy!
    本次发布,重点测试和验证了在java、kotlin、groovy三种jvm语言里,开箱即用的特性。并发布SolonInitializr:https://solon.noear.org/start/(也即将发布idea插件)最......
  • 一次学俩Vue&Blazor:1.4基础-响应式数据
    一、声明式编程和响应式数据1、声明式编程逻辑层修改视图层元素属性值的方式有两种,一是命令式,先通过getElementById等方法获取元素对象,然后再修改对象的属性;二是声明式......
  • 11.4外围设备的中断请求
       IRQ是用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制。该机制称为中断处理。中断处理在硬件控制中担当着重要角色。因为如果没有中断处理,就有可能出......
  • 11.4 外围设备的中断请求
    IRQ是用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制。该机制称为中断处理。如果没有中断处理,就有可能出现处理无法顺畅进行的情况。从中断处理开始到请求中断......
  • 信息系统项目管理基础:2.1.4项目和战略的规划
    企业是一个复杂动态开放的系统,有效的项目管理活动必须能根据企业战略要求,它是服务于企业战略的项目管理方法,并要求企业从高层到基层每位员工参与,在全方位的项目管理信......
  • 1.4 单调栈
    84.柱状图中最大的矩形最大矩形的高度瓶颈可能在于各个柱子的高度,如图所示基于以上观察,一个朴素算法是:枚举每种可能的高度瓶颈1.1向左、右扩展宽度1.2擂台更新......
  • 11.4外围设备的中断请求
    IRQ是用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制。该机制称为中断处理。中断处理在硬件控制中担当着重要角色。因为如果没有中断处理,就有可能出现处理无法......