-
Notifications
You must be signed in to change notification settings - Fork 48
肉夹馍针对 IoC/DI 已经发布了相关的 NuGet,请跳转 Rougamo.DI 了解更多。
从 3.0 版本开始,肉夹馍的代码编织方式从内联织入变为代理织入,其中的差异就是内联织入是直接修改原有方法,在方法对应生命周期节点调用不同方法,而代理织入是先将原有方法M
拷贝一份并改名为$Rougamo_M
,然后修改原有方法代码调用拷贝后的方法$Rougamo_M
并在调用前后执行对应生命周期节点方法,下面是简化后的伪代码(实际代码更为复杂,有兴趣的自行反编译查看):
public class TestAttribute : MoAttribute { }
public class Cls
{
// 原方法
public async void M() => await Task.Yield();
// 内联织入后的方法
public async void M()
{
var test = new TestAttribute();
var context = new MethodContext();
test.OnEntry(context);
try
{
await Task.Yield();
test.OnSuccess(context);
}
catch (Exception e)
{
test.OnException(context);
}
finally
{
test.OnExit(context);
}
}
// 代理织入的方法,首先将原方法拷贝一份
public async void $Rougamo_M() => await Task.Yield();
// 然后修改原方法调用拷贝的方法并进行织入
public void M()
{
var test = new TestAttribute();
var context = new MethodContext();
test.OnEntry(context);
try
{
M();
test.OnSuccess(context);
}
catch (Exception e)
{
test.OnException(context);
}
finally
{
test.OnExit(context);
}
}
}
对于async void
方法,其他方法在调用它时是无法进行await
操作的,只能当做普通的void
返回值同步方法看待。所以在对async void
应用肉夹馍时,肉夹馍无法保证OnSuccess
和OnExit
一定在方法执行成功后和方法退出时执行,也无法保证OnException
在 M 抛出异常时一定能够执行。所以在对async void
应用肉夹馍时需要谨慎对待,确保肉夹馍能够达到你期望的目的。
肉夹馍在编译时会检查async void
方法上应用肉夹馍的情况,一旦发现便会生成一个 MSBuild 告警,输出到 MSBuild 输出窗口。如果你确实希望避免async void
上应用肉夹馍的情况,又觉得告警信息不够明显,可以在项目文件的PropertyGroup
节点新增一个子节点<FodyTreatWarningsAsErrors>true</FodyTreatWarningsAsErrors>
,这个配置会让Fody
产生的告警信息变为错误信息,会直接使得编译失败,从而确保不会遗漏这类情况。
说实话,没有什么好办法。一般而言是不推荐使用async void
的,即使是不使用肉夹馍的情况下。在一些特殊情况下,如果必须使用async void
同时希望应用肉夹馍,那么能够给到的建议就是再封装一层async Task
:
// 改造前
[Test]
static async void M()
{
await Task.Yield();
}
// 改造后
static async void M()
{
await _M();
}
[Test]
static async Task _M()
{
await Task.Yield();
}
在阅读上面async void
的说明时,你可能会有这样的疑问“为什么弃用内联织入改用代理织入,内联织入就不存在async void
的问题了”。是的,内联织入确实不存在这样的问题,3.0 之前也确实这么做的,但内联织入也存在着自己的问题。同样是Task
类型返回值,是否使用async/await
语法,其实际编译后的代码是大相径庭的,使用async/await
语法后,在编译时会生成一个状态机类型,而不使用async/await
语法时,其实际就是一个返回值为Task
的同步方法,在内联织入时会出现和现在async void
类似的情况。再就是 4.0 版本新增的异步切面功能,也需要代理织入的支持。再综合考虑async void
在逐步淘汰的背景,不进行额外支持并产生告警信息是较好的选择。
一个方法上可以应用多个肉夹馍类型,一般情况下我们并不需要关注它们的执行顺序,但某些场景下可能需要他们按指定的顺序执行。可以通过实现IFlexibleOrderable
接口并指定Order
字段,该字段值用于排序,值越大执行顺序越靠后:
public class TestAttribute : MoAttribute, IFlexibleOrderable
{
public double Order { get; set; } = 10;
}
需要注意的是,Order
指定的顺序是OnEntry
的执行顺序,默认情况下其他方法的执行顺序与OnEntry
相反,这种设定是符合大部分场景的,比如一个方法上应用了两个Attribute: Attr1
和Attr2
,它们都在OnEntry
中开启事务,在OnExit
中关闭事务,如果OnExit
不采取与OnEntry
相反的执行顺序,就会出现事务交叉的情况。如果希望OnSuccess/OnException/OnExit
按照OnEntry
的执行顺序执行,可以修改配置项中的reverse-call-nonentry
。
Attribute 允许我们在应用它的时候指定构造方法参数和属性参数,Rougamo 的每一个切面类型都包含一系列的配置信息,其中有些配置可以在应用 Attribute 时动态配置,比如上面介绍的 自定义执行顺序,目前 Rougamo 支持以下配置应用时动态指定:
-
Order
,实现IFlexibleOrderable
接口,用于动态指定切面类型的执行顺序 -
Flags
,实现IFlexibleModifierPointcut
接口,用于动态指定切面类型的方法匹配规则(粗颗粒度匹配规则) -
Pattern
,实现IFlexiblePatternPointcut
接口,用于动态指定切面类型的方法匹配规则(精确的 AspectN 表达式匹配规则)
public class TestAttribute : MoAttribute, IFlexibleOrderable, IFlexibleModifierPointcut, IFlexiblePatternPointcut
{
// 对于这些属性,可以指定默认值,也可以不指定
public double Order { get; set; } = 10;
// 如果没有动态设置 Flags 的需求,推荐直接使用 PointcutAttribute 指定匹配规则
public AccessFlags Flags { get; set; }
// 如果没有动态设置 Pattern 的需求,推荐直接使用 PointcutAttribute 指定匹配规则
public string? Pattern { get; set; }
}
肉夹馍的MethodContext
中保存了当前方法的上下文信息,其中包含了方法参数列表和方法返回值,肉夹馍使用object
存储这些未知类型的数据。我们知道将一个值类型对象保存为object
是要进行装箱操作的,而在 .NET 中,ref struct
不支持装箱/拆箱操作。基于这种情况,只有在不保存ref struct
值到MethodContext
才能继续使用肉夹馍。所以默认情况下,在编译时,当检测到拥有ref struct
类型的参数或返回值的方法应用了肉夹馍时,就会产生一个编译错误。此时有三种方式可以修复该问题:
-
在定义切面类型时通过
OptimizationAttribute
指定不需要MethodContext.Arguments
和MethodContext.ReturnValue
如果确定切面类型并不需要参数和返回值,直接在定义是切面类型时通过
OptimizationAttribute
声明,这样就不会将参数和返回值保存到MethodContext
中了。[Optimization(MethodContext = Omit.Arguments | Omit.ReturnValue)] public class TestAttribute : MoAttribute { }
-
在参数和返回值为
ref struct
的方法上应用SkipRefStructAttribute
如果你的切面类型需要
MethodContext
保存参数或返回值,那么就无法使用第一种方式,此时可以针对含有ref struct
参数或返回值的方法单独应用SkipRefStructAttribute
,表明该方法不记录参数和返回值。另外SkipRefStructAttribute
还可以应用到类和程序集上,实现更大范围的应用。[SkipRefStruct] public ReadOnlySpan<char> M(ReadOnlySpan<char> value) => default;
-
在配置文件中将
skip-ref-struct
设置为true
在确定当前程序集默认忽略
ref struct
的情况下,还可以通过配置项skip-ref-struct
为整个程序集进行默认配置,skip-ref-struct
设置为true
的效果等同于[assembly: SkipRefStruct]
。
之所以没有在遇到ref struct
时默认不记录对应值,是因为Omit
会影响肉夹馍的功能。当设置了Omit.Arguments
后就无法通过MethodContext.Arguments
来获取方法参数列表了,也无法重写和刷新参数列表;当设置了Omit.ReturnValue
后就无法通过MethodContext.ReturnValue
获取方法返回值了,也无法拦截方法执行或修改返回值或进行异常处理了。而这些被影响的功能可能是你需要的功能,产生编译错误是为了让开发人员明确知道这些影响,并在已知的情况下决定是否忽略参数和返回值。
-
如何让其他人在引用你的包后不再需要直接引用肉夹馍
在开发中间件时,你的项目在引用肉夹馍后,手动修改项目文件中NuGet引用部分,增加
PrivateAssets="contentfiles"
。需要注意的时,使用你的中间件的项目必须直接引用你的 NuGet 包,间接引用无效。<PackageReference Include="Rougamo.Fody" Version="2.0.0" PrivateAssets="contentfiles" />
修改方法参数值 | 方法执行拦截 | 修改方法返回值 | 处理方法异常 | 重试执行方法 | 记录方法返回值 | 刷新方法参数 | |
---|---|---|---|---|---|---|---|
构造方法 | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
属性/getter/setter | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
同步方法 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 仅out/ref参数 |
异步方法 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
迭代器方法 | ✅ | ❌ | ❌ | ❌ | ❌ | 配置项iterator-returns
|
❌ |
异步迭代器方法 | ✅ | ❌ | ❌ | ❌ | ❌ | 配置项iterator-returns
|
❌ |