zoukankan      html  css  js  c++  java
  • Dora.Interception: 一个为.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:     {
      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: }

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

    image

  • 相关阅读:
    fullCalendar改造计划之带农历节气节假日的万年历(转)
    Linked List Cycle
    Remove Nth Node From End of List
    Binary Tree Inorder Traversal
    Unique Binary Search Trees
    Binary Tree Level Order Traversal
    Binary Tree Level Order Traversal II
    Plus One
    Remove Duplicates from Sorted List
    Merge Two Sorted Lists
  • 原文地址:https://www.cnblogs.com/artech/p/dora-interception.html
Copyright © 2011-2022 走看看