您当前的位置: 首页 >  .net

寒冰屋

暂无认证

  • 1浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

CrossCutterN:.NET的轻量级AOP工具

寒冰屋 发布时间:2022-08-19 19:15:00 ,浏览量:1

目录

介绍

背景

例子

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

实现AOP模块

准备AOP模块配置

准备目标模块配置

执行控制台应用工具

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

实现AOP模块

准备AOP模块配置

准备目标模块配置

执行控制台应用工具

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

运行时AOP方法调用切换

更多细节

注意事项

兴趣点

  • 下载 .NET Core 2.1 的示例
  • 下载示例表单 .NET Framework 4.6.1
介绍

随着面向切面编程 (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的概念,并且可能有一些使用过PostSharp、Spring AOP等AOP框架的经验。

例子

要将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";
    }
}

请注意,IExecution和IReturn接口由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.Sample。Target\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.json”和“target.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尝试解决这个问题,否则可以提供此功能。
  • DotNetCore和DotNetStandard:由于Mono.Cecil尚不支持netstandard的强名称,因此强名称功能不适用于netstandard分支。此外,由于Mono.Cecil中对泛型方法的支持不完整,因此某些元数据构建器接口无法从IBuilder接口继承。
  • CI支持:我还没有找到任何支持构建多个目标的免费CI环境,包括net461和netcore2.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

关注
打赏
1665926880
查看更多评论
立即登录/注册

微信扫码登录

0.0478s