.NET静态代码织入——肉夹馍(Rougamo)发布2.2
肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应用启动的初始化时间让服务更快可用,同时还能对静态方法进行AOP操作。
上一篇文章至此共发布两个版本,本篇文章中将依次介绍2.1和2.2版本中新增的功能。
2.1
Pattern增强-支持Attribute匹配
在2.0版本推出了 表达式匹配 功能,支持通过字符串表达式匹配方法。2.0版本共支持method
、getter
、setter
、property
、execution
和regex
六种匹配规则,之后在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
进行初始化之外,还会在执行OnSuccess
和OnException
之前也进行一次更新,保证各阶段获取到的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属性包含了Args
、RewriteArgs
或FreshArgs
中的任意一个,那么就表示需要使用到参数,此时指定该值将无效,参数依旧会被存储下来All
,会遗弃所有数据,当前版本就是Mos和Arguments,后续如果有增加枚举项,也将包含在内
使用优化-静默内置属性
依旧是github社区朋友反馈的issue,在使用肉夹馍封装中间件后,其他人在使用中间件定义的Attribute时IDE会提示出MoAttribute
内置的属性,情况大致如下图:
MoAttribute
内置的几个属性(Features
、Flags
、Order
、Pattern
)都提示出来了,虽然说这几个属性在应用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内置属性的开发者,更新到该版本之后将会产生报错,请及时按需按上面的方式重新将需要的属性进行公开。由于不必要的属性提示确实会给中间件开发者带来困扰,且该功能也无法平滑过度,所以只能一刀切,对受到影响的开发者深表抱歉。