中文 | English
Rougamo是一个静态代码编织的AOP组件,同为AOP组件较为常用的有Castle、Autofac、AspectCore等,与这些组件不同的是,这些组件基本都是通过动态代理+IoC的方式实现AOP,是运行时完成的,而Rougamo是编译时直接修改目标方法织入IL代码实现AOP的。肉夹馍支持包括同步/异步方法、静态/实例方法、构造方法、属性内的所有种类的方法。
也没什么由来,方法级AOP在我看来就是两面包夹芝士,在方法执行前后添加切面,将整个方法夹在中间。肉夹馍大家都吃过的,虽然叫肉夹馍,但其实是肉夹于馍,外层为馍中间为肉,Rougamo同理,外层切面我称为Mo
,中间实际方法称之为Rou
,希望Rougamo的馍能够为你们的肉增香添色。
- 优势
- **更短的应用启动时间:**静态编织发生在编译期,而动态代理发生在应用启动时;
- **支持所有方法:**支持包括构造方法、属性、静态方法之内的所有方法。动态代理由于依赖IoC,往往只能实现对实例方法的切面操作;
- **独立于其他组件,无需初始化:**动态AOP依赖于IoC组件,往往需要在应用启动时进行初始化,且不同的IoC组件还有着不同的初始化方式,肉夹馍无需初始化,定义好切面类型直接应用即可;
- 劣势
肉夹馍作为一款AOP组件,其基本功能就是在方法几个主要生命周期节点执行额外操作,肉夹馍支持四个生命周期节点(或称为连接点 Join Point):
- 方法执行前;
- 方法执行成功后;
- 方法抛出异常后;
- 方法退出时(无法方法执行成功还是抛出异常,都会进入该生命周期节点,类似
try..finally..
)。
用一个简单示例进行说明生命周期节点在代码中的表现形式:
// 定义一个类型继承MoAttribute
public class TestAttribute : MoAttribute
{
public override void OnEntry(MethodContext context)
{
// OnEntry对应方法执行前
}
public override void OnException(MethodContext context)
{
// OnException对应方法抛出异常后
}
public override void OnSuccess(MethodContext context)
{
// OnSuccess对应方法抛出异常后
}
public override void OnExit(MethodContext context)
{
// OnExit对应方法退出时
}
}
在方法的各个生命周期节点,除了能够执行类似记录日志、统计方法耗时、APM埋点等不影响方法执行的操作,还可以进行以下这些控制方法执行的操作:
最简单直接的方式便是将定义的Attribute
直接应用到方法上,这个方法可以是同步的也可以是异步的,可以是实例方法也可以是静态方法,可以是属性也可以是属性getter/setter,可以是示例构造方法也可以是静态构造方法:
class Abc
{
[Test]
static Abc() { }
[Test]
public Abc() { }
[Test]
public int X { get; set; }
public static Y
{
[Test]
get;
[Test]
set;
}
[Test]
public void M() { }
[Test]
public static async ValueTask MAsync() => await Task.Yeild();
}
直接应用到方法上的方式简单明了,但对于一些非常常用的AOP类型,每个方法都需要添加Attribute就显得非常麻烦,所以肉夹馍还提供了多种批量应用的方式:
我们在进行批量应用的时候,比如将TestAttribute
直接应用到一个类上,我们一般并不是希望这个类的所有方法都应用TestAttribute
,而是希望选定那些满足特定特征的部分方法。肉夹馍在方法筛选上提供了两种方案:
- 粗粒度的方法特征匹配,可以指定匹配静态、实例、共有、私有、属性、构造方法等一个或多个粗粒度特征;
- 类AspectJ表达式匹配,提供了类似AspectJ格式的表达式,可以进行更为细致的匹配,比如方法名称、返回值类型、参数类型等。
现在异步编程在日常开发中已经是非常常见的了,在最上面的示例中,方法生命周期节点对应的方法全是同步的,如果希望进行一些异步操作,就不得不手动阻塞异步操作等待返回结果了,这是存在一定的资源浪费和性能损耗的。肉夹馍除了同步切面,还提供了异步切面,更好的支持异步操作:
// 定义一个类型继承AsyncMoAttribute
public class TestAttribute : AsyncMoAttribute
{
public override async ValueTask OnEntryAsync(MethodContext context) { }
public override async ValueTask OnExceptionAsync(MethodContext context) { }
public override async ValueTask OnSuccessAsync(MethodContext context) { }
public override async ValueTask OnExitAsync(MethodContext context) { }
}
不过需要注意的是,如果并不需要做异步操作,那么还是推荐使用同步切面,更多关于异步切面的说明,请跳转 异步切面。
肉夹馍是方法级AOP组件,在对方法应用肉夹馍后,每次调用方法都会实例化肉夹馍的相关对象,增加GC的负担。虽然这些负担是轻微的,很多时候是可以忽略不计的,但肉夹馍关注其对性能的影响,所以从不同的方向提供了各种优化方式:
- 部分编织,如果你只是想在执行方法前记录一下调用日志,那么其他生命周期节点以及异常处理等功能实际并用不上,此时通过部分编织功能仅选择你需要功能进行编织,这样可以缩减实际编织的IL代码,同时减少运行时实际执行的指令数量;
- 结构体,类与结构体的其中一个区别便是,类分配在堆中,而结构体在栈中,使用结构体可以使肉夹馍的部分类型分配到栈中,减少GC压力;
- 瘦身MethodContext,
MethodContext
中保存了当前方法的一些上下文信息,这些信息需要使用额外的对象进行保存,同时伴随着一些装箱拆箱操作,比如方法参数、返回值等,如果确定不需要这些信息,可以通过瘦身MethodContext
达到一定的优化效果; - 强制同步,在异步切面中介绍了异步切面与同步切面的关系,异步切面虽然使用了
ValueTask
来优化同步执行,但终究还是存在额外的开销,在编写异步切面时,如果确定不需要异步操作,可以通过强制调用同步切面来避免异步切面的额外开销。