NET Core度身定制的AOP框架

NET Core度身定制的AOP框架

多年从事框架设计开发使我有了一种强迫症,那就是见不得一个应用里频繁地出现重复的代码。之前经常Review别人的代码,一看到这样的程序,我就会想如何将这些重复的代码写在一个地方,然后采用“注入”的方式将它们放到需要的程序中。我们知道AOP是解决这类问题最理想的方案。为此,我自己写了一个AOP框架,该框架被命名为Dora.Interception。Dora.Interception已经在GitHub上开源,如果有兴趣的朋友想下载源代码或者阅读相关文档,可以访问GitHub地址:https://github.com/jiangjinnan/Dora。Demo源代码下载地址:http://files.cnblogs.com/files/artech/Dora.Interception.Demo.rar

目录
一、Dora, 为什么叫这个名字?
二、Dora.Interception的设计目标
三、以怎样的方式使用Dora.Interception
四、如何定义一个Interceptor
五、定义InterceptorAttribute
六、应用InterceptorAttribute
七、以Dependency Injection的形式提供Proxy

一、Dora, 为什么叫这个名字?

其实我最早的想法是创建一个IoC框架,并将它命名为Doraemon(哆啦A梦),因为我觉得一个理想的IoC Container就像是机器猫的二次元口袋一样能够提供给你期望的一切服务对象。后来觉得这名字太长,所以改名为Dora。虽然Dora这个名字听上去有点“娘”,并且失去了原本的意思,但是我很喜欢这个单词的一种释义——“上帝的礼物”之一。在接触了.NET Core的时候,我最先研究的就是它基于ServiceCollection和ServiceProvider的Dependency Injection框架,虽然这个框架比较轻量级,但是能够满足绝大部分项目的需求,所以我放弃了初衷。不过我依然保留了Dora这个开源项目名,并为此购买了一个域名(doranet.org),我希望将我多年的一些想法以一系列开源框架的形式实现出来,Dora.Interception就是Dora项目的第一个基于AOP的框架。

二、Dora.Interception的设计目标

我当初在设计Dora.Interception框架时给自己确定的几个目标:

  • Dora.Interception一个基于运行时(Run Time),而不是针对编译时(Compile Time)的AOP框架。它通过在运行时动态创建代理对象(Proxy)来封装目标对象(Target),并自动注入应用的拦截器(Interceptor),而不是在编译时帮助你生成一个Proxy类型。
  • Dora.Interception需要采用一种优雅的方式来定义和应用Interceptor。
  • 能够与.NET Core的Dependency Injection框架无缝集成
  • 能够整合其他AOP框架。实际上Dora.Interception并没有自行实现最底层的“拦截”机制,我使用的是Castle的DynamicProxy。如果有其他的选择,我们可以很容易地将它引入进来。

三、以怎样的方式使用Dora.Interception

Dora.Interception目前的版本为1.1.0,由如下两个NuGet包来承载,由于Dora.Interception.Castle依赖于Dora.Interception,所以安装后者即可。

  • Dora.Interception: 提供基本的API
  • Dora.Interception.Castle: 提供基于Castle(DynamicProxy)的拦截实现

四、如何定义一个Interceptor

接下来我们通过一个简单的实例来说明一下如何采用“优雅”的方式来定义一个Interceptor类型。我们即将定义的这个CacheInterceptor可以应用到某个具有返回值的方法上实现针对返回值的缓存。如果应用了这个Interceptor,它根据传入的参数对返回的值实施缓存。如果后续调用传入了相同的参数,并且之前的缓存尚未过期,缓存的结果将直接作为方法的返回值,从而避免了针对目标方法的重复调用。针对的缓存功能实现在如下这个CacheInterceptor类型中,可以看出针对的缓存是利用MemoryCache来完成的。

   1: public class CacheInterceptor
   2: {
   3:     private readonly InterceptDelegate _next;
   4:     private readonly IMemoryCache _cache;
   5:     private readonly MemoryCacheEntryOptions _options;
   6:  
   7:     public CacheInterceptor(InterceptDelegate next, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
   8:     {
   9:         _next = next;
  10:         _cache = cache;
  11:         _options = optionsAccessor.Value;
  12:     }
  13:  
  14:     public async Task InvokeAsync(InvocationContext context)
  15:     {
  16:         if (!context.Method.GetParameters().All(it => it.IsIn))
  17:         {
  18:             await _next(context);
  19:         }
  20:  
  21:         var key = new Cachekey(context.Method, context.Arguments);
  22:         if (_cache.TryGetValue(key, out object value))
  23:         {
  24:             context.ReturnValue = value;
  25:         }
  26:         else
  27:         {
  28:             await _next(context);
  29:             _cache.Set(key, context.ReturnValue, _options);
  30:         }

31:     }
32: public class CacheKey {...}

  33: }

CacheInterceptor体现了一个典型的Interceptor的定义方式:

  • Interceptor类型无需实现任何的接口,我们只需要定义一个普通的公共实例类型即可。
  • Interceptor类型必须具有一个公共构造函数,并且该构造函数的第一个参数的类型必须是InterceptDelegate,后者代表的委托对象会帮助我们调用后一个Interceptor或者目标方法(如果当前Interceptor已经是最后一个了)。
  • 上述这个构造函数可以包含任意的参数(比如CacheInterceptor构造函数中的cache和optionsAccessor)。这些参数可以直接利用.NET Core的Dependency Injection的方式进行注册,对于没有注册的参数需要在应用该Interceptor的时候显式提供。
  • 拦截功能实现在约定的InvokeAsync的方法中,这是一个返回类型为Task的异步方法,它的第一个参数类型为InvocationContext,代表当前方法调用的上下文。我们可以利用这个上下文对象得到Proxy对象和目标对象,代表当前调用方法的MethodInfo对象,以及传入的输入参数等。除此之外,我们也可以利用这个上下文直接设置方法的返回值或者输出参数。
  • 这个InvokeAsync方法可以包含任意后续参数,但是要求这些参数预先以Dependency Injection的形式进行注册。这也是我没有定义一个接口来表示Interceptor的原因,因为这样就不能将依赖的服务直接注入到InvokeAsync方法中了。
  • 当前Interceptor是否调用后续的Interceptor或者目标方法,取决于你是否调用构造函数传入的这个InterceptDelegate委托对象。

由于依赖的服务对象(比如CacheInterceptor依赖IMemoryCache 和IOptions<MemoryCacheEntryOptions>对象)可以直接注入到InvokeAsync方法中,所以上述这个CacheInterceptor也可以定义成如下的形式

   1: public class CacheInterceptor
   2: {
   3:     private readonly InterceptDelegate _next;
   4:     public CacheInterceptor(InterceptDelegate next)
   5:     {
   6:         _next = next;
   7:     }
   8:  
   9:     public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
  10:     {
  11:         if (!context.Method.GetParameters().All(it => it.IsIn))
  12:         {
  13:             await _next(context);
  14:         }
  15:  
  16:         var key = new Cachekey(context.Method, context.Arguments);
  17:         if (cache.TryGetValue(key, out object value))
  18:         {
  19:             context.ReturnValue = value;
  20:         }
  21:         else
  22:         {
  23:             await _next(context);
  24:             _cache.Set(key, context.ReturnValue, optionsAccessor.Value);
  25:         }
  26:     }
  27: }

五、定义InterceptorAttribute

我们采用Attribute的形式来将对应的Intercepor应用到某个类型或者方法上,每个具体的Interceptor类型都具有对应的Attribute。这样的Attribute直接继承基类InterceptorAttribute。如下这个CacheReturnValueAttribute就是上面这个CacheInterceptor对应的InterceptorAttribute。

   1: [AttributeUsage(AttributeTargets.Method)]
   2: public class CacheReturnValueAttribute : InterceptorAttribute
   3: {
   4:     public override void Use(IInterceptorChainBuilder builder)
   5:     {
   6:         builder.Use<CacheInterceptor>(this.Order);
   7:     }
   8: }

具体的InterceptorAttribute只需要重写Use方法将对应的Interceptor添加到Interceptor管道之中,这个功能可以直接调用作为参数的InterceptorChainBuilder对象的泛型方法Use<TInterceptor>来实现。对于这个泛型方法来说,泛型参数类型代表目标Interceptor的类型,而第一个参数表示注册的Interceptor在整个管道中的位置。如果创建目标Interceptor而调用的构造函数的参数尚未采用Dependency Injection的形式注册,我们需要在这个方法中提供。对于CacheInterceptor依赖的两个对象(IMemoryCache 和IOptions<MemoryCacheEntryOptions>)都可以采用Dependency Injection的形式注入,所以我们在调用Use<CacheInterceptor>方法是并不需要提供这个两个参数。

假设我们定义一个ExceptionHandlingInterceptor来实施自动化异常处理,当我们在创建这个Interceptor的时候需要提供注册的异常处理类型的名称,那么我们需要采用如下的形式来定义对应的这个IntercecptorAttribute。如下面的代码片段所示,我们在调用Use<ExceptionHandlingInterceptor>方法的时候就需要显式指定这个策略名称。

   1: [AttributeUsage(AttributeTargets.Method|AttributeTargets.)]
   2: public class HandleExceptionAttribute : InterceptorAttribute
   3: {
   4:     public string ExceptionPolicy {get;}
   5:     public string HandleExceptionAttribute(string exceptionPolicy)
   6:     {
   7:         this.ExceptionPolicy = exceptionPolicy;
   8:     }
   9:     public override void Use(IInterceptorChainBuilder builder)
  10:     {
  11:         builder.Use<ExceptionHandlingInterceptor>(this.Order,this.ExceptionPolicy);
  12:     }
  13: }

有的时候,IntercecptorAttribute在注册对应Interceptor的时候需要使用到应用到当前方法或者类型上的其他Attribute。举个简单的例子,上述的这个HandleExceptionAttribute实际上是自动提供异常处理策略名称,假设异常处理系统自身使用另外一个独立的ExceptionPolicyAttribute采用如下的形式来提供这个策略。

   1: public class Foobar
   2: {
   3:    [ExceptionPolicy("DefaultPolicy")
   4:    public void Invoke()
   5:    {
   6:       ...
   7:    }
   8: }

这个问题很好解决,因为InterceptorAttribute自身提供了应用到目标方法或者类型上的所有Attribute,所以上述这个HandleExceptionAttribute可以采用如下的定义方式。

   1: [AttributeUsage(AttributeTargets.Method)]
   2: public class HandleExceptionAttribute : InterceptorAttribute
   3: {
   4:     public override void Use(IInterceptorChainBuilder builder)
   5:     {
   6:         ExceptionPolicyAttribute  attribute = this.Attributes.ofType<ExceptionPolicyAttribute>().First();
   7:         builder.Use<Exception>(this.Order, attribute.ExceptionPolicy);
   8:     }
   9: }

六、应用InterceptorAttribute

Interceptor通过对应的InterceptorAttribute被应用到某个方法或者类型上,我们在应用InterceptorAttribute可以利用其Order属性确定Interceptor的排列(执行)顺序。如下面的代码片段所示, HandleExceptionAttribute和CacheReturnValueAttribute分别被应用到Foobar类型和Invoke方法上,我要求ExceptionHandlingInterceptor能够处理CacheInterceptor抛出的异常, 那么前者必须由于后者执行,所以我通过Order属性控制了它们的执行顺序。值得一提的是,目前我们支持两个拦截机制,一种是基于接口,另一种是基于虚方法。如果采用基于接口的拦截机制,我要求InterceptorAttribute应用在实现类型或者其方法上,应用在接口和其方法上的InterceptorAttribute将无效。

   1: [HandleException("defaultPolicy", Order = 1)]
   2: public class Foobar: IFoobar
   3: {
   4:     [CacheReturnValue(this.Order = 2)]
   5:     public Data LoadData()
   6:     {
   7:         ...
   8:     }
   9: }

如果我们在类型上应用了某个InterceptorAttribute,但是对应的Interceptor却并不希望应用到某个方法中,我们可以利用NonInterceptableAttribute采用如下的形式将它们屏蔽,

   1: [CacheReturnValue]
   2: public class Foobar
   3: { 
   4:     ...
   5:     [NonInterceptable(typeof(CacheReturnValueAttribute)]
   6:     public Data GetRealTypeData()
   7:     {...}
   8: }

七、以Dependency Injection的形式提供Proxy

我们知道应用在目标类型或者其方法上的Interceptor能够生效,要求方法调用针对的是封装目标对象的Proxy对象,换句话说我们希望提供的对象是一个Proxy而不是目标对象。除此之外,我们在上面的设计目标已经提到过,我们希望这个AOP框架能够与.NET Core的Dependency Injection框架进行无缝集成,所以现在的问题变成了:如何让Dependency Injection的ServiceProvider提供的是Proxy对象,而不是目标对象。我提供的两种方案来解决这个问题,接下来我们通过一个ASP.NET Core MVC应用来举例说明。

为了能够使用上面提供的CacheInterceptor并且能够以很直观的方式感受到缓存的存在,我定义了如下这个表示系统时钟的ISystemClock接口和具体实现类型SystemClock。从如下的代码片段可以看出,GetCurrentTime方法总是返回实时的时间,但是由于应用了CaheReturnValueAttribute,如果CacheInterceptor生效,返回的时间在缓存过期之前总是相同的。

   1: public interface ISystomClock
   2: {
   3:     DateTime GetCurrentTime();
   4: }
   5:  
   6: public class SystomClock : ISystomClock
   7: {
   8:     [CacheReturnValue]
   9:     public DateTime GetCurrentTime()
  10:     {
  11:         return DateTime.UtcNow;
  12:     }
  13: }

我们在HomeController中以构造器注入的方式来使用ISystemClock。在默认情况下,如果我们注入的类型ISystemClock接口,那么毫无疑问,那么GetCurrentTime方法调用的就是SystemClock对象本身,所以根本不可能起到缓存的作用。所以我们将注入类型替换成IInterceptable<ISystomClock>,后者的Proxy属性将会返回我们希望的Proxy对象。

   1: public class HomeController : Controller
   2: {
   3:     private readonly ISystomClock _clock;
   4:     public HomeController(IInterceptable<ISystomClock> clockAccessor)
   5:     {
   6:         _clock = clockAccessor.Proxy;
   7:     }
   8:  
   9:     [HttpGet("/")]
  10:     public async Task Index()
  11:     {
  12:         this.Response.ContentType = "text/html";
  13:         await this.Response.WriteAsync("<html><body><ul>");
  14:         for (int i = 0; i < 5; i++)
  15:         {
  16:             await this.Response.WriteAsync($"<li>{_clock.GetCurrentTime()}({DateTime.UtcNow})</li>");
  17:             await Task.Delay(1000);
  18:         }
  19:         await this.Response.WriteAsync("</ul><body></html>");
  20:     }
  21: }

当然我们需要注册Dora.Interception一些必须的服务,这些服务采用如下的形式通过调用扩展方法AddInterception来实现。

   1: public class Startup
   2: {
   3:     public void ConfigureServices(IServiceCollection services)
   4:     {
   5:         services
   6:             .AddScoped<ISystomClock, SystomClock>()
   7:             .AddInterception(builder=>builder.SetDynamicProxyFactory())
   8:             .AddMvc();
   9:     }
  10:     public void Configure(IApplicationBuilder app)
  11:     {
  12:         app.UseMvc();
  13:     }
  14: }

虽然IInterceptable<T>能够解决Proxy的提供问题,但是这种编程模式其实是很不好的。理想的编程模式应该是:依赖某个服务就注入对应的服务接口就可以。这个问题其实也好解决,我们首先将HomeController还原成典型的编程模式:

   1: public class HomeController : Controller
   2: {
   3:     private readonly ISystomClock _clock;
   4:     public HomeController(ISystomClock clock)
   5:     {
   6:         _clock = clock;
   7:     }
   8:  
   9:     [HttpGet("/")]
  10:     public async Task Index()
  11:     {
  12:         this.Response.ContentType = "text/html";
  13:         await this.Response.WriteAsync("<html><body><ul>");
  14:         for (int i = 0; i < 5; i++)
  15:         {
  16:             await this.Response.WriteAsync($"<li>{_clock.GetCurrentTime()}({DateTime.UtcNow})</li>");
  17:             await Task.Delay(1000);
  18:         }
  19:         await this.Response.WriteAsync("</ul><body></html>");
  20:     }
  21: }

接下来我们只需要修改Startup的ConfigureServices的两个地方同样达到相同的目的。如下面的代码片段所示,我们让ConfigureServices返回一个IServiceProvider对象,这个对象直接调用我们定义的扩展方法BuilderInterceptableServiceProvider来创建。

   1: public class Startup
   2: {
   3:     public IServiceProvider ConfigureServices(IServiceCollection services)
   4:     {
   5:         services
   6:             .AddScoped<ISystomClock, SystomClock>()
   7:             .AddMvc();
   8:         return services.BuilderInterceptableServiceProvider(builder => builder.SetDynamicProxyFactory());
   9:     }
  10:  
  11:     public void Configure(IApplicationBuilder app)
  12:     {
  13:         app.UseMvc();
  14:     }
  15: }

对于上述的两种编程模式,运行程序后浏览器上都会呈现出相同的时间:

时间: 2024-08-09 10:36:40

NET Core度身定制的AOP框架的相关文章

Dora.Interception, 为.NET Core度身打造的AOP框架[4]:演示几个典型应用

为了帮助大家更深刻地认识Dora.Interception,并更好地将它应用到你的项目中,我们通过如下几个简单的实例来演示几个常见的AOP应用在Dora.Interception下的实现.对于下面演示的实例,它们仅仅是具有指导性质的应用,所以我会尽可能地简化,如果大家需要将相应的应用场景移植到具体的项目开发中,需要做更多的优化.源代码从这里下载. 目录一.对输入参数的格式化二.对参数的自动化验证三.对方法的返回值进行自动缓存 一.对输入参数的格式化 我们有一些方法对输入参数在格式上由一些要求,但

Dora.Interception,为.NET Core度身打造的AOP框架 [2]:以约定的方式定义拦截器

上一篇<更加简练的编程体验>提供了最新版本的Dora.Interception代码的AOP编程体验,接下来我们会这AOP框架的编程模式进行详细介绍,本篇文章着重关注的是拦截器的定义.采用“基于约定”的Interceptor定义方式是Dora.Interception区别于其他AOP框架的一个显著特征,要了解拦截器的编程约定,就得先来了解一下Dora.Interception中针对方法调用的拦截是如何实现的. 一.针对实例的拦截 总地来说,Dora.Interception针对方法调用的拦截机制

Dora.Interception,为.NET Core度身打造的AOP框架 [4]:与依赖注入框架的无缝集成

Dora.Interception最初的定位就是专门针对.NET Core的AOP框架,所以在整个迭代过程中我大部分是在做减法.对于.NET Core程序开发来说,依赖注入已经成为无处不在并且“深入骨髓”的东西,不论是在进行业务应用的开发,还是进行基础组件的开发,依赖注入是实现“松耦合”最为理想的方式(没有之一).对于绝大部分AOP框架来说,它们最终都会体现为创建一个能够拦截的“代理对象”来实现对方法调用的拦截,但是.NET Core中针对服务实例的提供完全由通过IServiceProvider

Dora.Interception,为.NET Core度身打造的AOP框架 [1]:更加简练的编程体验

很久之前开发了一个名为Dora.Interception的开源AOP框架(github地址:https://github.com/jiangjinnan/Dora,如果你觉得这个这框架还有那么一点价值,请不吝多点一颗星),最近对它作了一些改进(包括编程模式和性能,目前最新版本2.1.4).一直以来我对软件设计秉承的一个理念就是:好的设计应该是简单的设计.和其他AOP框架相比,虽然Dora.Interception提供的编程模式已经显得足够简单,但是我觉得还应该再简单点,再简单点.这个新版本对拦截

Dora.Interception,为.NET Core度身打造的AOP框架 [5]:轻松地实现与其他AOP框架的整合

这里所谓的与第三方AOP框架的整合不是说改变Dora.Interception现有的编程,而是恰好相反,即在不改变现有编程模式下采用第三方AOP框架或者自行实现的拦截机制.虽然我们默认提供基于IL Emit实现方式,并且对IL指令进行了深度的优化,但是如果我们真的具有更好的选择,我们可以通过简单的扩展完成对底层拦截机制改变. 一.IInterceptingProxyFactory 对于Dora.Interception来说,方法调用之所有能够被拦截的根源在于我们改变了服务实例的提供方式,原来的对

AOP框架Dora.Interception 3.0 [1]: 编程体验

.NET Core正式发布之后,我为.NET Core度身定制的AOP框架Dora.Interception也升级到3.0.这个版本除了升级底层类库(.NET Standard 2.1)之外,我还对它进行大范围的重构甚至重新设计.这次重构大部分是在做减法,其目的在于使设计和使用更加简单和灵活,接下来我们就来体验一下在一个ASP.NET Core应用程序下如何使用Dora.Interception. 源代码下载实例1(Console)实例2(ASP.NET Core MVC + 注册可拦截服务)实

基于 .NET 的开源AOP框架评估

Rating of Open Source AOPFrameworks in .NET 基于 .NET 的开源AOP框架评估 Introduction 引言 In the days where business agility is becoming the definite needof any business / IT infrastructure, quite frequentlywe are ending up with facing scenarios where we need t

AOP框架Dora.Interception 3.0 [3]: 拦截器设计

对于所有的AOP框架来说,多个拦截器最终会应用到某个方法上.这些拦截器按照指定的顺序构成一个管道,管道的另一端就是针对目标方法的调用.从设计角度来将,拦截器和中间件本质是一样的,那么我们可以按照类似的模式来设计拦截器. 一.InvocationContext 我们为整个拦截器管道定义了一个统一的执行上下文,并将其命名为InvocationContext.如下面的代码片段所示,我们可以利用InvocationContext对象得到方法调用上下文的相关信息,其中包括两个方法(定义在接口和实现类型),

AOP框架Dora.Interception 3.0 [4]: 基于特性的拦截器注册

按照单一职责的原则,拦截器只负责需要的拦截操作的执行,至于它采用何种方式应用到目标方法上,以及它在整个拦截器管道中的位置则属于“拦截器注册”的范畴.Dora.Interception提供了几种典型的注册方法,用户也可以根据自己的需求实现自己的注册方式. 一.IInterceptorProvider 一般来说,每一个拦截器类型都对应着一个IInterceptorProvider实现类型,后者利用其Use方法负责将前者放置到拦截器管道指定的位置.如下面的代码所示,IInterceptorProvid