Rougamo、Fody 实现静态Aop
最近在看项目,看到别人使用Rougamo框架,好奇花了点时间仔细研究了,在这里记录一下。
0. 静态编织 Aop
首先,我们先了解什么是Aop? Aop
是指面向切面编程 (Aspect Oriented Programming),而所谓的切面,可以认为是具体拦截的某个业务点。
我们常用的aop框架是 AspectCore,他是属于动态代理,也就是发生在运行时期间对代码进行“修改”。
Rougamo、Fody 是属于静态编织,是指
在编译阶段将代码修改或额外的功能直接嵌入到程序集中,这个过程发生在源代码被编译成可执行文件或库之前。这意味着,一旦编译完成,插入的代码就已经是程
序集的一部分,无需在运行时再进行额外的操作。
1. Rougamo 肉夹馍
Rougamo 是一个开源项目,github:
https://github.com/inversionhourglass/Rougamo
,他是通过Fody -> Mono.Cecil 的方式实现静态编织 实现Aop功能。
创建控制台程序,Nuget安装 Rougamo.Fody
[AttributeUsage(AttributeTargets.Method)]public classLoggingAttribute : MoAttribute
{public override voidOnEntry(MethodContext context)
{
Console.WriteLine("执行方法 {0}() 开始,参数:{1}.", context.Method.Name,
JsonConvert.SerializeObject(context.Arguments));
}public override voidOnException(MethodContext context)
{
Console.WriteLine("执行方法 {0}() 异常,{1}.", context.Method.Name, context.Exception.Message);
}public override voidOnExit(MethodContext context)
{
Console.WriteLine("执行方法 {0}() 结束.", context.Method.Name);
}public override voidOnSuccess(MethodContext context)
{
Console.WriteLine("执行方法 {0}() 成功.", context.Method.Name);
}
}internal classProgram
{static void Main(string[] args)
{
Add(1, 2);
AddAsync(1, 2);
Divide(1, 2);
}
[Logging]static int Add(int a, int b) => a +b;
[Logging]static Task<int> AddAsync(int a, int b) => Task.FromResult(a +b);
[Logging]static decimal Divide(decimal a, decimal b) => a /b;
}
运行后会自动创建FodyWeavers.xsd 和 FodyWeavers.xml
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> <xs:element name="Weavers"> <xs:complexType> <xs:all> <xs:element name="Rougamo" minOccurs="0" maxOccurs="1" type="xs:anyType" /> </xs:all> <xs:attribute name="VerifyAssembly" type="xs:boolean"> <xs:annotation> <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> </xs:annotation> </xs:attribute> <xs:attribute name="VerifyIgnoreCodes" type="xs:string"> <xs:annotation> <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> </xs:annotation> </xs:attribute> <xs:attribute name="GenerateXsd" type="xs:boolean"> <xs:annotation> <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> </xs:annotation> </xs:attribute> </xs:complexType> </xs:element> </xs:schema>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> <Rougamo /> </Weavers>
下面是运行结果
这时候我们可以看到 增加了LoggingAttribute 特性的方法在运行前、运行成功、运行结束 执行了 OnEntry(MethodContext context) 、OnSuccess(MethodContext context)、OnExit(MethodContext context) 方法,这时我们打开ILSpy工具,看看实际运行的代码
internal classProgram
{private static void Main(string[] args)
{
Add(1, 2);
AddAsync(1, 2);
Divide(1m, 2m);
}
[DebuggerStepThrough]private static int Add(int a, intb)
{
LoggingAttribute loggingAttribute= newLoggingAttribute();
IMo[] mos= new IMo[1] { loggingAttribute };
MethodContext methodContext= new MethodContext(null, typeof(Program), MethodBase.GetMethodFromHandle((RuntimeMethodHandle)/*OpCode not supported: LdMemberToken*/, typeof(Program).TypeHandle), isAsync: false, isIterator: false, mosNonEntryFIFO: false, mos, new object[2] { a, b });
loggingAttribute.OnEntry(methodContext);int result = default(int);if(methodContext.ReturnValueReplaced)
{
result= (int)methodContext.ReturnValue;
loggingAttribute.OnExit(methodContext);returnresult;
}if(methodContext.RewriteArguments)
{
a= (int)methodContext.Arguments[0];
b= (int)methodContext.Arguments[1];
}bool flag = default(bool);do{try{while (true)
{try{
flag= false;
result=$Rougamo_Add(a, b);
}catch(Exception exception)
{
methodContext.Exception=exception;
methodContext.Arguments[0] =a;
methodContext.Arguments[1] =b;
loggingAttribute.OnException(methodContext);if (methodContext.RetryCount > 0)
{continue;
}if(methodContext.ExceptionHandled)
{
result= (int)methodContext.ReturnValue;break;
}throw;
}break;
}
}finally{if (methodContext.HasException ||methodContext.ExceptionHandled)
{gotoIL_0160;
}
methodContext.ReturnValue=result;
methodContext.Arguments[0] =a;
methodContext.Arguments[1] =b;
loggingAttribute.OnSuccess(methodContext);if (methodContext.RetryCount <= 0)
{if(methodContext.ReturnValueReplaced)
{
result= (int)methodContext.ReturnValue;
}gotoIL_0160;
}
flag= true;gotoend_IL_00fc;
IL_0160:
loggingAttribute.OnExit(methodContext);
end_IL_00fc:;
}
}while(flag);returnresult;
}
[DebuggerStepThrough]private static Task<int> AddAsync(int a, intb)
{
LoggingAttribute loggingAttribute= newLoggingAttribute();
IMo[] mos= new IMo[1] { loggingAttribute };
MethodContext methodContext= new MethodContext(null, typeof(Program), MethodBase.GetMethodFromHandle((RuntimeMethodHandle)/*OpCode not supported: LdMemberToken*/, typeof(Program).TypeHandle), isAsync: false, isIterator: false, mosNonEntryFIFO: false, mos, new object[2] { a, b });
loggingAttribute.OnEntry(methodContext);
Task<int> result = default(Task<int>);if(methodContext.ReturnValueReplaced)
{
result= (Task<int>)methodContext.ReturnValue;
loggingAttribute.OnExit(methodContext);returnresult;
}if(methodContext.RewriteArguments)
{
a= (int)methodContext.Arguments[0];
b= (int)methodContext.Arguments[1];
}bool flag = default(bool);do{try{while (true)
{try{
flag= false;
result=$Rougamo_AddAsync(a, b);
}catch(Exception exception)
{
methodContext.Exception=exception;
methodContext.Arguments[0] =a;
methodContext.Arguments[1] =b;
loggingAttribute.OnException(methodContext);if (methodContext.RetryCount > 0)
{continue;
}if(methodContext.ExceptionHandled)
{
result= (Task<int>)methodContext.ReturnValue;break;
}throw;
}break;
}
}finally{if (methodContext.HasException ||methodContext.ExceptionHandled)
{gotoIL_015b;
}
methodContext.ReturnValue=result;
methodContext.Arguments[0] =a;
methodContext.Arguments[1] =b;
loggingAttribute.OnSuccess(methodContext);if (methodContext.RetryCount <= 0)
{if(methodContext.ReturnValueReplaced)
{
result= (Task<int>)methodContext.ReturnValue;
}gotoIL_015b;
}
flag= true;gotoend_IL_00fc;
IL_015b:
loggingAttribute.OnExit(methodContext);
end_IL_00fc:;
}
}while(flag);returnresult;
}
[DebuggerStepThrough]private static decimal Divide(decimal a, decimalb)
{
LoggingAttribute loggingAttribute= newLoggingAttribute();
IMo[] mos= new IMo[1] { loggingAttribute };
MethodContext methodContext= new MethodContext(null, typeof(Program), MethodBase.GetMethodFromHandle((RuntimeMethodHandle)/*OpCode not supported: LdMemberToken*/, typeof(Program).TypeHandle), isAsync: false, isIterator: false, mosNonEntryFIFO: false, mos, new object[2] { a, b });
loggingAttribute.OnEntry(methodContext);decimal result = default(decimal);if(methodContext.ReturnValueReplaced)
{
result= (decimal)methodContext.ReturnValue;
loggingAttribute.OnExit(methodContext);returnresult;
}if(methodContext.RewriteArguments)
{
a= (decimal)methodContext.Arguments[0];
b= (decimal)methodContext.Arguments[1];
}bool flag = default(bool);do{try{while (true)
{try{
flag= false;
result=$Rougamo_Divide(a, b);
}catch(Exception exception)
{
methodContext.Exception=exception;
methodContext.Arguments[0] =a;
methodContext.Arguments[1] =b;
loggingAttribute.OnException(methodContext);if (methodContext.RetryCount > 0)
{continue;
}if(methodContext.ExceptionHandled)
{
result= (decimal)methodContext.ReturnValue;break;
}throw;
}break;
}
}finally{if (methodContext.HasException ||methodContext.ExceptionHandled)
{gotoIL_0160;
}
methodContext.ReturnValue=result;
methodContext.Arguments[0] =a;
methodContext.Arguments[1] =b;
loggingAttribute.OnSuccess(methodContext);if (methodContext.RetryCount <= 0)
{if(methodContext.ReturnValueReplaced)
{
result= (decimal)methodContext.ReturnValue;
}gotoIL_0160;
}
flag= true;gotoend_IL_00fc;
IL_0160:
loggingAttribute.OnExit(methodContext);
end_IL_00fc:;
}
}while(flag);returnresult;
}
[Logging]private static int $Rougamo_Add(int a, intb)
{return a +b;
}
[Logging]private static Task<int> $Rougamo_AddAsync(int a, intb)
{return Task.FromResult(a +b);
}
[Logging]private static decimal $Rougamo_Divide(decimal a, decimalb)
{return a /b;
}
}
从实际运行的代码我们可以看到,原先Add(int a, int b)方法中的执行内容被移动到 $Rougamo_Add方法中,而Add(int a, int b)方法先是new LoggingAttribute() 和 new Rougamo.Context.MethodContext() -> 执行了 loggingAttribute.OnEntry(methodContext); -> 在do{}while(bool) 执行了$Rougamo_Add(a, b); -> 在 exception 中执行了loggingAttribute.OnException(methodContext); -> 在 finally中执行了 loggingAttribute.OnSuccess(methodContext); 和 loggingAttribute.OnExit(methodContext);
注:do{}while(bool) 执行了$Rougamo_Add(a, b); 是因为 Rougamo 可以实现方法执行失败重试功能
至此我们明白了 Rougamo 实现 Aop功能是通过编译时修改IL代码,往代码增加对应的生命周期代码。那他为什么可以做到呢?其实是借用了Fody -> Mono.Cecil 的方式。
代码如下:
https://gitee.com/Karl_Albright/csharp-demo/tree/master/FodyDemo/RougamoDemo
2. Fody -> Mono.Cecil
Fody 是一个开源项目,github:
https://github.com/Fody/Fody
,相关教程文档在
https://github.com/Fody/Home/tree/master/pages
创建类库,选择netstandard2.0,命名为HelloWorld,Nuget安装 Fody 和 FodyPackaging
注:必须创建 netstandard2.0,因为FodyPackaging的目标是netstandard2.0,
在HelloWorld项目中,我们只放 HWAttribute类,继承于 Attribute。代码如下
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method |AttributeTargets.Property)]public classHWAttribute : Attribute
{
}
再次创建类库,选择netstandard2.0,命名为HelloWorld.Fody,Nuget安装 FodyHelpers,引用HelloWorld类库
在HelloWorld.Fody项目中,我们只放ModuleWeaver类(类名是固定的,详情见Fody文档),继承于 BaseModuleWeaver。代码如下
usingFody;usingMono.Cecil;usingMono.Cecil.Cil;usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Reflection;namespaceHelloWorld.Fody
{public partial classModuleWeaver : BaseModuleWeaver
{public override voidExecute()
{foreach (var type inModuleDefinition.Types)
{foreach (var method intype.Methods)
{var customerAttribute = method.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name ==nameof(HWAttribute));if (customerAttribute != null)
{
ProcessMethod(method);
}
}
}
}public override IEnumerable<string>GetAssembliesForScanning()
{yield return "mscorlib";yield return "System";
}private MethodInfo _writeLineMethod => typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });private voidProcessMethod(MethodDefinition method)
{//获取当前方法体中的第一个IL指令 var processor =method.Body.GetILProcessor();var current =method.Body.Instructions.First();//插入一个 Nop 指令,表示什么都不做 var first =Instruction.Create(OpCodes.Nop);
processor.InsertBefore(current, first);
current=first;//构造 Console.WriteLine("Hello World") foreach (var instruction inGetInstructions(method))
{
processor.InsertAfter(current, instruction);
current=instruction;
}
}private IEnumerable<Instruction>GetInstructions(MethodDefinition method)
{yield returnInstruction.Create(OpCodes.Nop);yield return Instruction.Create(OpCodes.Ldstr, "Hello World.");yield returnInstruction.Create(OpCodes.Call, ModuleDefinition.ImportReference(_writeLineMethod));
}
}
}
在代码中,我们遍历了所有类型的所有方法,如果方法标注了 HWAttribute特性,则增加 Console.WriteLine("Hello World."); 代码。
创建控制台应用程序,命名为HelloWorldFodyDemo,添加 HelloWorld 和 HelloWorld.Fody 项目引用,并且手动增加 WeaverFiles标签,目标是HelloWorld.Fody.dll
在控制台中,我们需要一个方法,方法上有 HWAttribute 特性就可以了,代码如下
internal classProgram
{static void Main(string[] args)
{
Echo();
Console.ReadKey();
}
[HW]public static voidEcho()
{
Console.WriteLine("Hello Fody.");
}
}
在控制台项目中,我们还需要 FodyWeavers.xml 和 FodyWeavers.xsd 文件,(我也是从上面Rougamo项目中复制的),内容如下
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> <HelloWorld /> </Weavers>
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Weavers"> <xs:complexType> <xs:all> <xs:element name="HelloWorld" minOccurs="0" maxOccurs="1" type="xs:anyType" /> </xs:all> <xs:attribute name="VerifyAssembly" type="xs:boolean"> <xs:annotation> <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> </xs:annotation> </xs:attribute> <xs:attribute name="VerifyIgnoreCodes" type="xs:string"> <xs:annotation> <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> </xs:annotation> </xs:attribute> <xs:attribute name="GenerateXsd" type="xs:boolean"> <xs:annotation> <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> </xs:annotation> </xs:attribute> </xs:complexType> </xs:element> </xs:schema>
目前,文件结构如下
FodyDemo
|--- HelloWorld
|--- HWAttribute.cs
|--- HelloWorld.csproj
|--- HelloWorld.Fody
|--- HelloWorld.Fody.csproj
|--- ModuleWeaver.cs
|--- HelloWorldFodyDemo
|--- FodyWeavers.xml
|--- FodyWeavers.xsd
|--- HelloWorldFodyDemo.csproj
|--- Program.cs
代码如下:
https://gitee.com/Karl_Albright/csharp-demo/tree/master/FodyDemo
最后运行结果如下,很明显,HWAttribute生效了,我们成功的在Echo()方法前打印了Hello World。
我们再次打开ILSpy工具,得到的结果如图,代码增加了Console.WriteLine("Hello World.");行代码
4. Fody 有很多其他的“插件”,大家可以多试试
AutoProperties.Fody
: 这个外接程序为您提供了对自动属性的扩展控制,比如直接访问backing字段或拦截getter和setter。
PropertyChanged.Fody:
将属性通知添加到实现INotifyPropertyChanged的所有类。
InlineIL.Fody:
在编译时注入任意IL代码。
MethodDecorator.Fody:
通过IL重写编译时间装饰器模式。
NullGuard.Fody:
将空参数检查添加到程序集。
ToString.Fody:
给属性生成ToString()方法
Rougamo.Fody:
在编译时生效的AOP组件,类似于PostSharp。