.NET静态代码织入——肉夹馍(Rougamo) 发布1.4.0
肉夹馍(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
MoAttribute
和ExMoAttribute
都支持重写参数,需要注意的是,重写参数时需要将MethodContext.RewriteArguments
设置为true
,仅仅修改context.Arguments
元素是不生效的。
重试(v1.4.0)
1.4.0版本新增的功能可以让我们在遇到指定异常或者返回值非预期值的情况下重新执行当前方法,实现方式是在OnException
和OnSuccess
中设置MethodContext.RetryCount
值,在OnException
和OnSuccess
执行完毕后如果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,因为手动处理异常和修改返回值就表示你已经决定了该方法的最终结果,所以就不再需要重试了 - 的
OnEntry
和OnExit
只会执行一次,不会因为重试而多次执行,OnException
和OnSuccess
根据你设置的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 ),该项目封装出了RetryAttribute
和RecordRetryAttribute
两个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.AspNetCore
和Rougamo.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
也进行一些额外的处理
统一记录异常
如果记录异常的逻辑是通用的,那么每次实现IRecordableMatcher
或IRecordableRetryDefinition
接口时都要带上这段逻辑处理会有些麻烦,虽然说可以抽象父类,但还是会稍显麻烦而且有遗漏的可能。
考虑到这个问题Rougamo.Retry
也提供了统一记录异常的方式,那就是RecordRetryAttribute
和IRecordable
的组合。
// 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() { }
Rougamo.Retry注意事项
- 在使用
RetryAttribute
和RecordRetryAttribute
时,当前项目必须直接引用Rougamo.Retry
,不可间接引用,否则代码无法织入。 Rougamo.Retry
主要使用的是重试功能,同时RetryAttribute
和RecordRetryAttribute
继承自MoAttribute
而不是ExMoAttribute
,所以同样不推荐将这些Attribute应用到没有使用async/await语法糖的方法上
最后
感谢大家的使用和反馈,我最开始想到肉夹馍可以完成的AOP功能基本都实现了,没错,其实大部分功能都是在项目建立之初就想好了,只是因为比较懒,所以这1.0版本到现在1.4版本拖了一年多。
当然也不是说肉夹馍的功能就开发至此不再更新了,其实还有一个功能是计划中的,不过这个功能属于增强不属于前面那种全新的功能。如果大家有想到什么肉夹馍能做的功能,也可以到github上反馈,不过新功能的实现可能就比较拖沓了..
特别感谢在github上反馈问题的朋友们,也因为比较懒,所以测试用例的覆盖面也不是很全,好些BUG都是靠大家反馈的。1.4.0版本现已出发,那么BUG的探索就交给大家了,就算是懒,有BUG还是会尽快修复的。
.NET静态代码织入——肉夹馍(Rougamo) 发布1.4.0的更多相关文章
- .NET静态代码织入——肉夹馍(Rougamo) 发布1.1.0
肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应 ...
- .NET静态代码织入——肉夹馍(Rougamo) 发布1.2.0
肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应 ...
- .NET静态代码织入——肉夹馍(Rougamo)
肉夹馍是什么 肉夹馍通过静态代码织入方式实现AOP的组件..NET常用的AOP有Castle DynamicProxy.AspectCore等,以上两种AOP组件都是通过运行时生成一个代理类执行AOP ...
- 30个类手写Spring核心原理之AOP代码织入(5)
本文节选自<Spring 5核心原理> 前面我们已经完成了Spring IoC.DI.MVC三大核心模块的功能,并保证了功能可用.接下来要完成Spring的另一个核心模块-AOP,这也是最 ...
- Spring的LoadTimeWeaver(代码织入)
在Java 语言中,从织入切面的方式上来看,存在三种织入方式:编译期织入.类加载期织入和运行期织入.编译期织入是指在Java编译期,采用特殊的编译器,将切面织入到Java类中:而类加载期织入则指通过特 ...
- Spring的LoadTimeWeaver(代码织入)(转)
https://www.cnblogs.com/wade-luffy/p/6073702.html 在Java 语言中,从织入切面的方式上来看,存在三种织入方式:编译期织入.类加载期织入和运行期织入. ...
- 【开源】.Net Aop(静态织入)框架 BSF.Aop
BSF.Aop .Net 免费开源,静态Aop织入(直接修改IL中间语言)框架,类似PostSharp(收费): 实现前后Aop切面和INotifyPropertyChanged注入方式. 开源地址: ...
- Java AOP (1) compile time weaving 【Java 切面编程 (1) 编译期织入】
According to wikipedia aspect-oriented programming (AOP) is a programming paradigm that aims to inc ...
- AOP静态代理解析2-代码织入
当我们完成了所有的AspectJ的准备工作后便可以进行织入分析了,首先还是从LoadTimeWeaverAwareProcessor开始. LoadTimeWeaverAwareProcessor实现 ...
- 框架源码系列三:手写Spring AOP(AOP分析、AOP概念学习、切面实现、织入实现)
一.AOP分析 问题1:AOP是什么? Aspect Oriented Programming 面向切面编程,在不改变类的代码的情况下,对类方法进行功能增强. 问题2:我们需要做什么? 在我们的框架中 ...
随机推荐
- async.js 版本兼容问题 async.filter举例
async3.x 和async2.6.1 版本下 const files = ['dir1/file1.txt','dir2/file3.txt','dir3/file6.txt']; // Usin ...
- kali2021.4a安装angr(使用virtualenv)
在Linux中安装各种依赖python的软件时,最头疼的问题之一就是各个软件的python版本不匹配的问题,angr依赖python3,因此考虑使用virtualenv来安装angr Virtuale ...
- STM32标准库中GPIO_ReadInputData与GPIO_ReadInputDataBit的区别
GPIO_ReadInputData读的是GPIOx的整个IDR寄存器的数据,返回一个十六位数,对应IDR寄存器的十六位.反映GPIOx所有端口的电平状态,所以参数只用传入GPIOx. uint16_ ...
- IE浏览器卸载
1.打开此电脑,点击上箭头,打开控制面板: 2.选择卸载程序: 3.点击启用或关闭Windows功能: 4.弹出Windows功能对话框,找到Inetrnet Explorer 11,取消勾选: 5. ...
- 【转载】SQL 2012以上版本分页查询更简单
2012以上版本分页查询更简单 注意:以下都是先执行排序,再取行数据 select* from t_workers order by worker_id desc offset 3 rows -- ...
- JUC并发编程详解(通俗易懂)
一.JUC简介 在Java5.0提供了java.util.concurrent包,简称JUC,即Java并发编程工具包.JUC更好的支持高并发任务. 具体的有以下三个包: java.util.conc ...
- Maui 读取外部文件显示到Blazor中
Maui 读取外部文件显示到Blazor中 首先在maui blazor中无法直接读取外部文件显示 ,但是可以通过base64去显示 但是由于base64太长可能影响界面卡顿 这个时候我们可以使用bl ...
- Java安全之JDBC Attacks学习记录
Java安全之JDBC Attacks 写在前面 很早就看到了Make JDBC Attacks Brilliant Again议题,一直想分析学习下,但是太懒. MySQL 原理概述 "扩 ...
- ac自动姬
字符串 ac自动姬 前言 省选临近,不能再颓了! 说着开始研究起moonlight串流.真香 本期博客之所以在csdn上发了一份,因为没有图床!如果有图床我一定会自力更生的! 好像和字符串没有毛关系 ...
- Longhorn+K8S+KubeSphere云端数据管理,实战 Sentry PostgreSQL 数据卷增量快照/备份与还原
云端实验环境配置 VKE K8S Cluster Vultr 托管集群 https://vultr.com/ 3 个 worker 节点,kubectl get nodes. k8s-paas-71a ...