Skip to content
ihourglass edited this page Dec 16, 2024 · 7 revisions

其他

肉夹馍中使用IoC

肉夹馍针对 IoC/DI 已经发布了相关的 NuGet,请跳转 Rougamo.DI 了解更多。

async void特别说明

从 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应用肉夹馍时,肉夹馍无法保证OnSuccessOnExit一定在方法执行成功后和方法退出时执行,也无法保证OnException在 M 抛出异常时一定能够执行。所以在对async void应用肉夹馍时需要谨慎对待,确保肉夹馍能够达到你期望的目的。

如何避免async void方法应用肉夹馍

肉夹馍在编译时会检查async void方法上应用肉夹馍的情况,一旦发现便会生成一个 MSBuild 告警,输出到 MSBuild 输出窗口。如果你确实希望避免async void上应用肉夹馍的情况,又觉得告警信息不够明显,可以在项目文件的PropertyGroup节点新增一个子节点<FodyTreatWarningsAsErrors>true</FodyTreatWarningsAsErrors>,这个配置会让Fody产生的告警信息变为错误信息,会直接使得编译失败,从而确保不会遗漏这类情况。

async void方法如何优雅的使用肉夹馍

说实话,没有什么好办法。一般而言是不推荐使用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: Attr1Attr2,它们都在OnEntry中开启事务,在OnExit中关闭事务,如果OnExit不采取与OnEntry相反的执行顺序,就会出现事务交叉的情况。如果希望OnSuccess/OnException/OnExit按照OnEntry的执行顺序执行,可以修改配置项中的reverse-call-nonentry

应用Attribute时指定肉夹馍内置属性

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; }
}

特殊参数或返回值类型ref struct

肉夹馍的MethodContext中保存了当前方法的上下文信息,其中包含了方法参数列表和方法返回值,肉夹馍使用object存储这些未知类型的数据。我们知道将一个值类型对象保存为object是要进行装箱操作的,而在 .NET 中,ref struct不支持装箱/拆箱操作。基于这种情况,只有在不保存ref struct值到MethodContext才能继续使用肉夹馍。所以默认情况下,在编译时,当检测到拥有ref struct类型的参数或返回值的方法应用了肉夹馍时,就会产生一个编译错误。此时有三种方式可以修复该问题:

  1. 在定义切面类型时通过OptimizationAttribute指定不需要MethodContext.ArgumentsMethodContext.ReturnValue

    如果确定切面类型并不需要参数和返回值,直接在定义是切面类型时通过OptimizationAttribute声明,这样就不会将参数和返回值保存到MethodContext中了。

    [Optimization(MethodContext = Omit.Arguments | Omit.ReturnValue)]
    public class TestAttribute : MoAttribute
    {
    }
  2. 在参数和返回值为ref struct的方法上应用SkipRefStructAttribute

    如果你的切面类型需要MethodContext保存参数或返回值,那么就无法使用第一种方式,此时可以针对含有ref struct参数或返回值的方法单独应用SkipRefStructAttribute,表明该方法不记录参数和返回值。另外SkipRefStructAttribute还可以应用到类和程序集上,实现更大范围的应用。

    [SkipRefStruct]
    public ReadOnlySpan<char> M(ReadOnlySpan<char> value) => default;
  3. 在配置文件中将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
Clone this wiki locally