肉夹馍(
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内置属性的开发者,更新到该版本之后将会产生报错,请及时按需按上面的方式重新将需要的属性进行公开。由于不必要的属性提示确实会给中间件开发者带来困扰,且该功能也无法平滑过度,所以只能一刀切,对受到影响的开发者深表抱歉。

标签: none

添加新评论