CrossCutterN:.NET的轻量级AOP工具

1108 篇文章 54 订阅
624 篇文章 16 订阅

目录

介绍

背景

例子

使用方法名称查找要注入的目标方法

实现AOP模块

准备AOP模块配置

准备目标模块配置

执行控制台应用工具

使用自定义属性标记要注入的目标方法

实现AOP模块

准备AOP模块配置

准备目标模块配置

执行控制台应用工具

使用多个切面构建器执行AOP代码注入

运行时AOP方法调用切换

更多细节

注意事项

兴趣点


介绍

随着面向切面编程 (AOP)已成为编程中广为人知的常用概念,开发人员越来越依赖适当的AOP工具。

.NET编程中,最著名的AOP工具是PostSharp,它允许使用自定义属性注入自定义AOP代码。好东西总不是白送的,除了繁琐的手动获取证书的过程,PostSharp express版也有一些限制,让关心项目规模的开发者犹豫不决,而最终版本的价格将成为许多开发者的主要关注点。

为了有一个免费的.NET AOP工具,实现了CrossCutterN。它提供了AOP功能,其工作方式与大多数现有AOP工具略有不同。

CrossCutterN工具的优势包括:

  • 免费CrossCutterN是开源的,在MIT许可下免费。
  • 轻量级: CrossCutterN不是在项目中添加编译时依赖,而是在构建后阶段注入AOP代码。这种方法允许将AOP代码注入到源代码不可用的程序集中,并尽可能地将项目代码与AOP代码解耦。
  • 跨平台CrossCutterN适用于.NET Framework.NET Core环境。
  • 开箱即用的切面切换支持CrossCutterN允许用户在项目运行时以多个粒度级别打开/关闭注入方法/属性的AOP代码。
  • 专为优化性能而设计CrossCutterN使用IL编织技术使注入的AOP代码像在目标项目中直接编码一样高效,并优化实现以避免不必要的局部变量初始化和方法调用。

背景

本文假设读者熟悉切面AOP的概念,并且可能有一些使用过PostSharpSpring AOPAOP框架的经验。

例子

要将AOP代码编织到程序集中,CrossCutterN需要以下过程:

  • 按照CrossCutterN约定准备AOP代码模块。AOP代码内容完全由开发人员定制。
  • 准备AOP模块的配置文件。
  • 准备目标模块的配置文件,需要注入AOP代码。
  • 执行控制台应用工具,将原始程序集与AOP代码信息一起编织成一个新程序集。

然后就完成了。

我们以一个非常简单的C#方法为例:

namespace CrossCutterN.Sample.Target
{
    using System;

    internal class Target
    {
        public static int Add(int x, int y)
        {
            Console.Out.WriteLine("Add starting");
            var z = x + y;
            Console.Out.WriteLine("Add ending");
            return z;
        }
    }
}

执行时,控制台的输出为:

现在,如果我想向该Add方法注入一些AOP代码怎么办?例如,在进入方法调用时记录函数调用及其所有参数值,以及方法返回前的返回值?CrossCutterN目前提供了2种方法。

在每个示例中执行控制台应用程序工具之前,请确保重建示例目标项目,以获取CrossCutterN执行IL织入的新目标程序集。

使用方法名称查找要注入的目标方法

按照列出的步骤操作:

实现AOP模块

首先实现一些实用程序属性和方法:

namespace CrossCutterN.Sample.Advice
{
    using System;
    using System.Text;
    using CrossCutterN.Base.Metadata;

    internal sealed class Utility
    {
        internal static string CurrentTime => 
        DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff tt");

        internal static string GetMethodInfo(IExecution execution)
        {
            var strb = new StringBuilder(execution.Name);
            strb.Append("(");
            if (execution.Parameters.Count > 0)
            {
                foreach (var parameter in execution.Parameters)
                {
                    strb.Append(parameter.Name).Append("=").
                    Append(parameter.Value).Append(",");
                }

                strb.Remove(strb.Length - 1, 1);
            }

            strb.Append(")");
            return strb.ToString();
        }

        internal static string GetReturnInfo(IReturn rReturn) 
            => rReturn.HasReturn ? 
            $"returns {rReturn.Value}" : "no return";
    }
}

请注意,IExecutionIReturn接口由CrossCutterN.Base.dll程序集提供。要使CrossCutterN工具正常工作,开发人员必须遵循其约定并提供接口。

现在实现在输入时和方法返回之前输出日志的方法:

namespace CrossCutterN.Sample.Advice
{
    using System;
    using CrossCutterN.Base.Metadata;

    public static class AdviceByNameExpression
    {
        public static void OnEntry(IExecution execution)
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by method name on entry: {Utility.GetMethodInfo(execution)}");

        public static void OnExit(IReturn rReturn)
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by method name on exit: {Utility.GetReturnInfo(rReturn)}");
    }
}

只是为了方便演示,我们直接将日志输出到控制台。AOP模块实现完成。

准备AOP模块配置

json文件添加到AOP模块项目,确保它与程序集一起复制。将json文件命名为adviceByNameExpression.json

{
  "CrossCutterN": {
    "sample": {
      "AssemblyPath": "CrossCutterN.Sample.Advice.dll",
      "Advices": {
        "CrossCutterN.Sample.Advice.AdviceByNameExpression": {
          "testEntry": {
            "MethodName": "OnEntry",
            "Parameters": [ "Execution" ]
          },
          "testExit": {
            "MethodName": "OnExit",
            "Parameters": [ "Return" ]
          }
        }
      }
    }
  }
}

配置文件的含义如下:

  • 我有一个程序集,其中包含要注入的AOP代码,用于引用该程序集的键是sample
  • 这个程序集的路径是CrossCutterN.Sample.Advice.dll ”;它不是绝对路径,所以汇编路径与配置文件的路径有关,在这种情况下,它与配置文件在同一个文件夹中。
  • 它有以下AOP方法(即Advices)被注入到类CrossCutterN.Sample.Advice.AdviceByNameExpression中。
  • 一种名为OnEntry的方法,其中一种参数类型标记为Execution(即C#代码中的IExecution类型)。此方法将在目标程序集配置中称为testEntry
  • 一种名为“ OnExit的方法,其中一种参数类型标记为Return(即C#代码中的IReturn类型)。此方法将在目标程序集配置中称为testExit

准备目标模块配置

json文件添加到目标模块项目中,并确保将其与要注入AOP方法调用的程序集一起复制。将json文件命名为nameExpressionTarget.json ”

{
  "CrossCutterN": {
    "DefaultAdviceAssemblyKey": "sample",
    "AspectBuilders": {
      "aspectByMethodName": {
        "AspectBuilderKey": "CrossCutterN.Aspect.Builder.NameExpressionAspectBuilder",
        "Includes": [ "CrossCutterN.Sample.Target.Target.Ad*" ],
        "Advices": {
          "Entry": { "MethodKey": "testEntry" },
          "Exit": { "MethodKey": "testExit" }
        }
      }
    },
    "Targets": {
      "CrossCutterN.Sample.Target.exe": { "Output": "CrossCutterN.Sample.Target.exe" }
    }
  }
}

配置文件的含义如下:

  • 我有一个默认的AOP代码模块,可以称为sample
  • 以下AspectBuilders是为了帮助我进行注射而定义的。
  • 一个切面构建器可以称为CrossCutterN.Aspect.Builder.NameExpressionAspectBuilder。此参考由CrossCutterN工具实现和提供,该工具将通过检查方法的名称来找到注入AOP代码的方法。
  • 这个切面构建器将注入全名类似CrossCutterN.Sample.Target.Target.Ad*的所有方法
  • 这个切面构建器将向目标方法调用的Entry上注入一个方法调用,该方法调用可以被称为testEntry
  • 此切面构建器把向一个方法调用注入到一个方法,该方法可以在目标方法调用的Exit之前引用为testExit
  • 此切面构建器添加的AOP代码可以在配置中称为aspectByMethodName以用于订购和C#代码以打开/关闭。
  • 一个程序集位于要注入的Targets程序集中。程序集是CrossCutterN.Sample.Target.exe。它不是绝对路径,所以路径与配置文件有关,在这种情况下,它在配置文件的同一文件夹中。编织的程序集将保存为CrossCutterN.Sample.Target.exe ”,与配置文件相关的路径,在这种情况下也是配置文件的同一文件夹。输出程序集的文件名与目标程序集完全相同,因此原始程序集将被编织的程序集覆盖。请注意,EXE用于.NET框架示例。对于.NET Core示例,它应该是CrossCutterN.Sample.Target.dll

执行控制台应用工具

使用Release配置构建AOP和目标程序集,导航到CrossCutterN.Sample\folder,执行:

CrossCutterN.Console\CrossCutterN.Console.exe /d:CrossCutterN.Sample.Advice\bin\Release\adviceByNameExpression.json /t:CrossCutterN.Sample.Target\bin\Release\nameExpressionTarget.json

该命令的含义是:

执行CrossCutterN的控制台应用程序,使用CrossCutterN.Sample.Advice\bin\Release\adviceByNameExpression.json 文件作为AOP代码组装配置(/d:不是D:盘,表示此配置用于AOP代码组装),并使用CrossCutterN.SampleTarget\bin\Release\nameExpressionTarget.json文件作为目标程序集配置(/t:表示目标程序集配置)。

对于.NET Core示例,命令如下所示:

dotnet CrossCutterN.Console\CrossCutterN.Console.dll /d:CrossCutterN.Sample.Advice\bin\Release\netstandard2.0\adviceByNameExpression.json /t:CrossCutterN.Sample.Target\bin\Release\netcoreapp2.1\nameExpressionTarget.json

如果执行成功,则将原来的CrossCutterN.Sample.Target.exe文件替换为新生成的文件。执行新程序集,预计会出现以下输出:

结果表明AOP方法调用已成功注入。

要保留原始目标程序集以进行比较或其他目的,只需将目标程序集配置中的Targets部分中的Output配置更改为原始程序集名称以外的其他值,在这种情况下,可能是CrossCutterN.Sample.Target.Weaved.exe ”或其他东西。请注意,虽然CrossCutterN输出程序集和pdb文件,但它不处理程序集的配置文件。如果用户决定不覆盖原始程序集,则需要复制原始exe.config文件并将其重命名以匹配新的EXE程序集名称,以使用新名称执行EXE程序集。

使用自定义属性标记要注入的目标方法

CrossCutterN工具还提供了一种使用自定义属性标记要注入的目标方法的方法。并且过程与前面类似。

实现AOP模块

namespace CrossCutterN.Sample.Advice
{
    using System;
    using CrossCutterN.Base.Concern;
    using CrossCutterN.Base.Metadata;

    public static class AdviceByAttribute
    {
        public static void OnEntry(IExecution execution) 
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by attribute on entry: {Utility.GetMethodInfo(execution)}");

        public static void OnExit(IReturn rReturn) 
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by attribute on exit: {Utility.GetReturnInfo(rReturn)}");
    }

    public sealed class SampleConcernMethodAttribute : ConcernMethodAttribute
    {
    }
}

请注意,这一次,声明了一个用于标记目标方法的属性SampleConcernMethodAttribute。该属性应添加到目标Add方法。

namespace CrossCutterN.Sample.Target
{
    using System;

    internal class Target
    {
        [CrossCutterN.Sample.Advice.SampleConcernMethod]
        public static int Add(int x, int y)
        {
            Console.Out.WriteLine("Add starting");
            var z = x + y;
            Console.Out.WriteLine("Add ending");
            return z;
        }
    }
}

准备AOP模块配置

{
  "CrossCutterN": {
    "sample": {
      "AssemblyPath": "CrossCutterN.Sample.Advice.dll",
      "Attributes": { "method": "CrossCutterN.Sample.Advice.SampleConcernMethodAttribute" },
      "Advices": {
        "CrossCutterN.Sample.Advice.AdviceByAttribute": {
          "entry1": {
            "MethodName": "OnEntry",
            "Parameters": [ "Execution" ]
          },
          "exit1": {
            "MethodName": "OnExit",
            "Parameters": [ "Return" ]
          }
        }
      }
    }
  }
}

Attributes节中,定义了一个CrossCutterN.Sample.Advice.SampleConcernMethodAttribute类型的属性来标记目标方法。在目标配置中可以称为method。配置文件名是adviceByAttribute.json

准备目标模块配置

{
  "CrossCutterN": {
    "DefaultAdviceAssemblyKey": "sample",
    "AspectBuilders": {
      "aspectByAttribute": {
        "AspectBuilderKey": "CrossCutterN.Aspect.Builder.ConcernAttributeAspectBuilder",
        "ConcernMethodAttributeType": { "TypeKey": "method" },
        "Advices": {
          "Entry": { "MethodKey": "entry1" },
          "Exit": { "MethodKey": "exit1" }
        }
        //,"IsSwitchedOn": false
      }
    },
    "Targets": {
      "CrossCutterN.Sample.Target.exe": { "Output": "CrossCutterN.Sample.Target.exe" }
    }
  }
}

这里,AspectBuilderKey改为CrossCutterN.Aspect.Builder.ConcernAttributeAspectBuilder,也是CrossCutterN工具实现和提供的,它会通过检查预定义的属性来查找标记的方法。配置文件是attributeTarget.json

不过,EXE是用于.NET Framework示例的。对于.NET Core示例,它应该是CrossCutterN.Sample.Target.dll

执行控制台应用工具

使用Release配置构建AOP和目标程序集,导航到CrossCutterN.Sample\folder,执行:

CrossCutterN.Console\CrossCutterN.Console.exe /d:CrossCutterN.Sample.Advice\bin\Release\adviceByAttribute.json /t:CrossCutterN.Sample.Target\bin\Release\attributeTarget.json

对于.NET Core示例,命令如下所示:

dotnet CrossCutterN.Console\CrossCutterN.Console.dll /d:CrossCutterN.Sample.Advice\bin\Release\netstandard2.0\adviceByAttribute.json /t:CrossCutterN.Sample.Target\bin\Release\netcoreapp2.1\attributeTarget.json

执行编织程序集时的预期结果与前面的示例类似:

使用多个切面构建器执行AOP代码注入

当然要注入多个AOP方法调用,可以在单个AOP程序集配置文件和单个目标程序集配置文件中声明多个切面构建器。请检查示例项目中的advice.jsontarget.json配置文件。忽略详细过程以减少文本冗余。

不过要提一件事,要让多个切面构建器一起工作,必须指定AOP方法调用顺序,例如target.json中的Order部分:

"Order": {
  "Entry": [
    "aspectByAttribute",
    "aspectByMethodName"
  ],
  "Exit": [
    "aspectByMethodName",
    "aspectByAttribute"
  ]
}

这意味着当对一个目标方法应用多个切面构建器时,在进入时,首先应用切面构建器(称为aspectByAttribute)注入的方法调用,然后应用由切面构建器(称为aspectByMethodName)注入的方法调用。并且在退出目标方法调用之前,将注入的AOP方法调用顺序按照配置倒过来。请注意,对于目标配置文件中的单个切面构建器,可以忽略Order部分,但对于目标配置文件中的多个切面构建器是必需的。

运行时AOP方法调用切换

如果有时用户打算暂时禁用某些AOP方法调用并稍后启用它们,CrossCutterN提供了一种在程序运行时打开和关闭注入的AOP方法调用的方法。

注意示例中的//,"IsSwitchedOn": false配置项,它是这种切换的配置条目:

  • 如果未指定,则切面构建器注入的AOP方法调用将不可切换,这意味着它们总是在触发目标方法时执行。
  • 如果设置为false,则切面构建器注入的AOP方法类将是可切换的,但默认情况下不执行,除非在运行时打开。它们可以在程序运行期间打开和关闭。
  • 如果设置为true,则切面构建器注入的AOP方法调用将是可切换的,并且默认情况下会执行,除非在运行时关闭。它们可以在程序运行期间关闭和打开。

所以我们取消注释这个配置条目,保存配置文件,再过一遍使用自定义属性标记要注入的目标方法示例,编织程序集的输出将不包括AOP输出:

在程序中,在调用Add方法之前执行以下语句:

CrossCutterN.Base.Switch.SwitchFacade.Controller.SwitchOn("aspectByAttribute");

请注意,aspectByAttribute是我们用来在目标配置中引用切面构建器的键。再次执行使用自定义属性标记要注入的目标方法示例,编织程序集的输出将再次包含AOP输出。

更多细节

以上只是CrossCutterN工具的简单演示。

对于AOP代码注入,它可以通过各种配置选项在入口点、异常点和出口点注入方法和属性,以便通过方法/属性/构造函数、可访问性和静态/实例轻松包含/排除注入目标。

对于AOP代码切换操作,它允许各种粒度,例如切换切面构建器注入的所有AOP代码、注入到类、方法中的所有AOP代码等。

在某些情况下,某些对象必须在入口、异常或出口的建议之间传递,使用参数类型CrossCutterN.Base.Metadata.IExecutionContext声明的注入建议就是为此目的而设计的。此接口允许通知使用对象键将对象存储在其中,并在稍后调用的通知中检索、更新或删除它。

CrossCutterN比上面的介绍更加灵活、可配置和可扩展。有兴趣的读者,请访问GitHub下载源代码并了解更多详细信息。

考虑到这个工具还在不断发展,它的文档很可能并不完美,而且尽管编写并通过了测试用例,但可能存在缺陷。如果在使用工具过程中发现任何问题,或者有任何问题、建议和要求,请不要犹豫对本文发表评论,向项目提交问题,或发送电子邮件至keeper013@ gmail.com

注意事项

  • 请不要使用此工具注入已注入的程序集。以上面提到的程序集CrossCutterN.SampleTarget.exe为例,如果使用CrossCutterN工具对该程序集注入两次,则不能保证它仍然可以完美运行。
  • 不能保证CrossCutterN可以与任何其他AOP工具一起使用。
  • 用多线程的方式来做这个AOP代码注入过程是没有意义的,因为开发者倾向于基于CrossCutterN源代码开发自己的工具,请注意AOP代码注入部分根本不是为多线程设计的(为什么有人想要2个线程注入一个程序集)。
  • AOP代码切换特性考虑并实现了多线程。
  • 无法保证CrossCutterN可以与混淆工具一起使用。

兴趣点

  • 自定义MsBuild任务:拥有一个msbuild任务无疑有助于将工具更容易地集成到项目中。情况是目前的msbuild工具有一个程序集绑定重定向问题,自定义msbuild任务不适用于某些程序集绑定重定向,不幸的是CrossCutterN就是其中之一(主要用于json配置功能)。要么msbuild解决了这个问题,要么CrossCutterN尝试解决这个问题,否则可以提供此功能。
  • DotNetCoreDotNetStandard:由于Mono.Cecil尚不支持netstandard的强名称,因此强名称功能不适用于netstandard分支。此外,由于Mono.Cecil中对泛型方法的支持不完整,因此某些元数据构建器接口无法从IBuilder接口继承。
  • CI支持:我还没有找到任何支持构建多个目标的免费CI环境,包括net461netcore2.0。此外,正如appveyor所说,它还不支持dotnet测试,所以目前CI不适用于该项目的netstandard分支(本文构建netcore示例),但该分支实际上在本地测试是可以的(所有NUnit测试用例通过)。
  • Weaver 接口设计:目前,CrossCutterN.Weaver.Weaver.IWeaver接口需要文件名而不是输入和输出程序集的流。这是因为当前Mono.Cecil支持完全使用流输出带有pdb文件的编织程序集,这导致了当前的设计。这可以在Mono.Cecil更新后批准。

https://www.codeproject.com/Tips/1187601/CrossCutterN-A-Light-Weight-AOP-Tool-for-NET

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值