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

时间:2024-01-22 08:59:06

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

上一篇文章至此共发布两个版本,本篇文章中将依次介绍2.1和2.2版本中新增的功能。

2.1

Pattern增强-支持Attribute匹配

在2.0版本推出了 表达式匹配 功能,支持通过字符串表达式匹配方法。2.0版本共支持methodgettersetterpropertyexecutionregex六种匹配规则,之后在github上收到社区朋友的issue反馈,希望能够直接筛选出应用了某个Attribute的方法,因此在2.1版本中新增匹配规则attr

attr的基本格式为attr(POS TYPE)

  • TYPE,表示我们匹配的Attribute类型,其格式与其他匹配规则中类型的格式相同
  • POS,表示应用Attribute的位置,我们知道Attribute可以应用于程序集、类、方法、属性、字段、参数、返回值等,POS支持以下几种位置
    • type,表示Attribute应用于类型上
    • exec,表示Attribute应用于方法或属性或属性getter或属性setter上(后续统一简称方法)
    • para x,表示Attribute应用于方法参数上,其中x表示参数的位置,0表示第一个参数,x*时表示任意参数
    • ret,表示Attribute应用于方法返回值上

下面展示几个简单的表达式示例:

// 1. 匹配类型上应用了类名为ObsoleteAttribute(任意命名空间)的类型,选取该类型下的所有方法
"attr(type ObsoleteAttribute)"

// 2. 匹配应用了Test.XAttribute(限定Test命名空间)的方法
"attr(exec Test.XAttribute)"

// 3. 匹配方法第3个参数(参数索引从0开始)上应用了Restrict命名空间下类名以Attribute结尾的任意Attribute的方法
"attr(para 2 Restrict.*Attribute)"

// 4. 匹配方法返回值上应用了任意Attribute的方法
"attr(ret *)"

// 5. 联合其他匹配规则完成更复杂的匹配
"attr(type ObsoleteAttribute) && method(public * *(..))"

我们知道,在应用Attribute时对于同一个Attribute类型,我们还可以设置其构造参数和属性,不同的构造参数和属性所表达的含义可能完全不同,我们可能会有希望能够更详细的匹配其参数值和属性值的需求。但遗憾的是,肉夹馍目前没有计划在这个目标上继续细化,表达式匹配相对于最早的AccessFlags提供了更灵活细化的匹配方式,但同样增加了学习成本和复杂度,目前是希望在灵活度和复杂度上取一个平衡,不希望表达式演变到晦涩难懂,也同样希望能满足基本的需求。当然,另一方面来说,相对简单的表达式也为后续的维护省下不少精力。对于确实有需要对参数值和属性值进行细分处理的,目前建议在OnEntry等方法中通过MethodContext.Method获取更多方法相关信息进行处理。

参数值实时更新

这同样是github上社区的朋友通过issue反馈的,方法包含out参数值,该参数在OnSuccess中无法从MethodContext.Arguments中获取到。

在此前的版本中,MethodContext.Arguments实际只有在执行OnEntry前会进行一次初始化,之后并不会对其进行更新,也就是说实际上不仅out的参数值没有更新,其他参数同样没有更新。可能有的朋友之前也从MethodContext.Arguments中获取过参数值并且发现是最新的,那大概是因为你这个参数是引用类型,你只是修改了应用对象的内部值,而不是直接为这个参数重新赋值。

2.1版本除了在执行OnEntry之前对MethodConetext.Arguments进行初始化之外,还会在执行OnSuccessOnException之前也进行一次更新,保证各阶段获取到的Arguments都是最新的。该功能在升级到2.1+版本后自动生效。

这样的更新操作势必要额外产生一些代码的,所以Feature同时新增枚举项FreshArgs,如果你并不需要这个刷新参数值的功能,可以通过排除该枚举值来减少代码的织入,对Feature不理解的朋友可以回顾往期的 部分织入 介绍。

2.2

2.2版本的主要内容是性能优化,同样是来自github社区朋友的issue反馈,使用肉夹馍后GC相应的也增加了。在2.2版本从引入结构体、延迟初始化(没用到则不初始化)、减少装箱等各方面进行优化,下面介绍的是我们使用上的一些变化,内部的细节优化这里不做阐述。

结构体

结构体大家都知道,但是使用可能并不是很频繁,类对象是分配在堆上的,最后由GC回收,而结构体是保存在栈上的,在调用栈结束后直接释放不走GC,所以一般结构体的效率是优于类的。关于类和结构体有一个很有趣的事情分享一下,我们知道C#项目在编译时可以选择Debug模式和Release模式,Debug模式下包含了很多调试信息,并且不会对我们的代码进行优化,而Release模式则会精简很多,无意义的分支跳转甚至都会被优化掉,不仅如此,Release模式下还使用结构体代替类对异步方法进行了优化。每个异步方法都会生成一个实现了IAsyncStateMachine的类型,在Debug模式下生成的类型时一个class,而Release模式则是struct,感兴趣的同学可以反编译看看,这里不再展开了。

那么回归主题,既然结构体这么好,那是不是无脑将class改为struct就好了呢,答案显而易见,不然就不会有class了。结构体是值传递,一个结构体在方法间进行传递时实际是复制了一个副本进行传递,那么最直观的表现就是你将一个结构体传入方法M,方法M内部修改了结构体的字段,但是在方法M执行完毕后,你外部的结构体字段并没有变化。再回到肉夹馍中,肉夹馍定义的类型主要包含两个,一个是继承了MoAttribute定义AOP内容的类型,另一个是MethodContext,其中MethodContext用于在多个MoAttribute子类之间传递,所以无法改造为结构体,那么结构体的优化就落在了MoAttribute上了。

MoAttribute继承自Attribute,而Attribute是一个class,MoAttribute是无法直接定义为struct的。如果对肉夹馍已经比较熟悉的朋友可能知道,MoAttribute除了继承自Attribute还实现了IMo接口,同时肉夹馍除了可以MoAttribute直接应用于类或方法上的方式之外,还有一种方式是让被织入类型实现空接口IRougamo<T>,而IRougamo<T>的泛型约束是where T : IMo, new(),所以最本质上还是IMo这个接口。那么下面的例子展示了如何使用结构体进行优化:

// 1. 定义结构体实现IMo接口
struct ValueMo : IMo
{
    // 实现接口,定义AOP操作
}

// 2.1. 通过RougamoAttribute指定结构体类型
[Rougamo(typeof(ValueMo))]
class Cls
{
    // 如果项目使用C#11及以上语法,可以直接使用下面这种泛型Attribute
    [Rougamo<ValueMo>]
    public void M() { }
}

// 2.2. 同样可以通过IRougamo<T>配合使用
class Clss : IRougamo<ValueMo>
{
}

遗弃部分数据

2.0版本中介绍了部分织入的功能,我们可以通过选择自己需要的功能来减少织入的IL代码量。现在,我们还可以选择丢掉部分我们不需要的数据。MethodContext中保存了方法上下文信息,同样的,也不是所有信息大家都会需要。在2.2版本中,将部分相对有性能开销的部分数据设置为可选,在实现IMo接口或继承MoAttribute时可以通过MethodContextOmits属性进行设置,该属性类型为枚举,包含以下枚举项:

  • None,不会遗弃任何数据,默认值
  • Mos,会遗弃MethodContext.Mos属性值,该属性包含了当前方法织入的所有IMo对象,如果你使用了结构体,那么在存储到该属性中时将包含一个装箱操作,同时也会增加一个IReadOnlyList<IMo>对象。需要注意的是,使用ExMoAttribute时请务必不要指定该值
  • Arguments,会遗弃MethodContext.Arguments属性值,该属性存储了调用放方法的所有入参值,如果入参中包含值类型,那么将产生一个装箱操作,同时也会增加一个object[]对象。需要注意的是,如果Features属性包含了ArgsRewriteArgsFreshArgs中的任意一个,那么就表示需要使用到参数,此时指定该值将无效,参数依旧会被存储下来
  • All,会遗弃所有数据,当前版本就是Mos和Arguments,后续如果有增加枚举项,也将包含在内

使用优化-静默内置属性

依旧是github社区朋友反馈的issue,在使用肉夹馍封装中间件后,其他人在使用中间件定义的Attribute时IDE会提示出MoAttribute内置的属性,情况大致如下图:

MoAttribute内置的几个属性(FeaturesFlagsOrderPattern)都提示出来了,虽然说这几个属性在应用Attribute时进行设置是肉夹馍提供的功能之一,但是如果咱们的中间件已经为某些属性设置了默认值,同时不希望开发者在应用Attribute时再去修改。比如上图中,如果RetryAttribute在定义时已经重写了Feature属性,限定了要启用的功能,就不希望使用RetryAttribute的人再修改该值,最好的方法就是让IDE都不提示出这个属性,但此时又会发现这些属性根本无法屏蔽。虽然可以人为提醒开发者不要设置该属性,但这在使用上非常不友好。

这个现象主要原因是MoAttribute将属性的getter和setter都设置为了public,所有继承自MoAttribute的类都无法屏蔽public的setter,所以在2.2版本中,MoAttribute的所有属性的setter都修改为private,是否将属性公开由继承MoAttribute的类型决定,可以通过new关键字覆盖原属性然后将setter改为public:

public class OrderableAttribute : MoAttribute
{
    // 通过new关键字覆盖了MoAttribute的Order属性,同时将setter设置为public
    // virtual关键字是可选的,如果你确定没有类型会继承自TestAttribute并重写Order属性,那么可以去掉virtual
    // 这里为Order设置了一个默认值,当然也可以不设置
    public new virtual double Order { get; set; } = 10;
}

注意:这个功能会影响到现在在应用Attribute时设置MoAttribute内置属性的开发者,更新到该版本之后将会产生报错,请及时按需按上面的方式重新将需要的属性进行公开。由于不必要的属性提示确实会给中间件开发者带来困扰,且该功能也无法平滑过度,所以只能一刀切,对受到影响的开发者深表抱歉。