NET Core写了一个轻量级的Interception框架[开源]

ASP.NET Core具有一个以ServiceCollection和ServiceProvider为核心的依赖注入框架,虽然这只是一个很轻量级的框架,但是在大部分情况下能够满足我们的需要。不过我觉得它最缺乏的是针对AOP的支持,虽然这个依赖注入框架提供了扩展点使我们可以很容易地实现与第三方框架的集成,但是我又不想“节外生枝”,为此我们趁这个周末写了一个简单的Interception框架来解决这个问题。通过这个命名为Dora.Interception的框架,我们可以采用一种非常简单、直接而优雅地(呵呵)在这个原生的DI框架上实现针对AOP的编程。目前这只是一个Beta(Beta1)版本,我将它放到了github上(https://github.com/jiangjinnan/Dora)。我写这篇文章不是为了说明这个Dora.Interception的设计和实现原理,而是为了介绍如何利用它在一个ASP.NET Core与原生的DI框架结合实现AOP的编程模式。两个实例可以从这里下载。

目录
一、基本原理
二、安装NuGet包
三、定义Interceptor
四、定义InterceptorAttribute
五、以DI的方式注入代理
六、如果你不喜欢IInterceptable<T>接口

一、基本原理

和大部分针AOP/Interception的实现一样,我们同样采用“代理”的方式实现对方法调用的拦截和注入。如下图所示,我们将需要以AOP方法注入的操作定义成一个个的Interceptor,并以某种方式(我采用的是最为直接的标注Attribute的形式)应用到某个类型或者方法上。在运行的时候我们为目标对象创建一个代理,我们针对代理对象的调用将会自动传递到目标对象。不过在目标对象最终被调用的时候,注册的Interceptor会按照顺序被先后执行。

二、安装NuGet包

这个框架目前涉及到如下两个框架,基础的模型实现在Dora.Interception这个包中,Dora.Interception.Castle则利用Castle.DynamicProxy针对代理的创建提供了一个默认实现。

  • Dora.Interception
  • Dora.Interception.Castle

这两个NuGet包已经上传到nuget.org,所以我们可以直接使用它们。假设我们创建了一个空的ASP.NET Core控制台应用,我们可以通过执行如下的命名

三、定义Interceptor

假设我们创建这样一个Interceptor,它能够捕获后续执行过程中抛出的异常,并将异常消息写入日志,我们将这个Interceptor命名为ErrorLogger。如下所示的就是这个ErrorLogger的完整定义。

   1: public class ErrorLogger
   2: {
   3:     private InterceptDelegate _next;
   4:     private ILogger _logger;
   5:     public ErrorLogger(InterceptDelegate next, ILoggerFactory loggerFactory, string category)
   6:     {
   7:         _next     = next;
   8:         _logger   = loggerFactory.CreateLogger(category);
   9:     }
  10:  
  11:     public async Task InvokeAsync(InvocationContext context)
  12:     {
  13:         try
  14:         {
  15:             await _next(context);
  16:         }
  17:         catch (Exception ex)
  18:         {
  19:             _logger.LogError(ex.Message);
  20:             throw;
  21:         }
  22:     }
  23: }

考虑到依赖注入的使用,我们并没有为具体的Interceptor类型定义一个接口,用户仅仅需要按照如下的约定来定义这个Interceptor类型就可以了。对ASP.NET Core的管道设计比较熟悉的人应该可以看出这与中间件的设计是一致的。

  • Interceptor具有一个这样一个公共构造函数:它的第一个参数是一个InterceptDelegate 类型的委托,我们通过它调用后续的Interceptor或者目标对象。我们并不对后续的参数做任何约束,它们可以采用DI的方式进行注入(比如上面的loggerFactory参数)。如果不能以DI的形式提供的参数(比如参数category),在后面注册的时候需要显式指定。
  • 拦截注入的功能虚线实现在一个名为InvokeAsync的方法中,该方法的需要返回一个Task对象,并且要求方法中包含一个类型为InvocationContext 的对象,该对象表示执行代理方法的执行上下文。如下面的代码片段所示,我们不仅仅可以得到与当前方法调用相关的上下文信息,还可以直接利用它设置参数的值和最终返回的值。InvokeAsync方法需要自行决定是否继续调用后续的Interceptor和目标对象,这可以直接通过在构造函数中指定的这个InterceptDelegate 来完成。
   1: namespace Dora.Interception
   2: {
   3:     public abstract class InvocationContext
   4:     {
   5:         protected InvocationContext();
   6:  
   7:         public abstract object[] Arguments { get; }
   8:         public abstract Type[] GenericArguments { get; }
   9:         public abstract object InvocationTarget { get; }
  10:         public abstract MethodInfo Method { get; }
  11:         public abstract MethodInfo MethodInvocationTarget { get; }
  12:         public abstract object Proxy { get; }
  13:         public abstract object ReturnValue { get; set; }
  14:         public abstract Type TargetType { get; }
  15:  
  16:         public abstract object GetArgumentValue(int index);
  17:         public abstract void SetArgumentValue(int index, object value);
  18:     }
  19: }

由于构造函数和InvokeAsync方法都支持依赖注入,所以ErrorLogger也可以定义成如下的形式(ILoggerFactory 在InvokeAsync方法中注入)。

   1: public class ErrorLogger
   2: {
   3:     private InterceptDelegate _next;
   4:     private string  _category;
   5:     public ErrorLogger(InterceptDelegate next,  string category)
   6:     {
   7:         _next = next;
   8:         _category = category;
   9:     }
  10:  
  11:     public async Task InvokeAsync(InvocationContext context, ILoggerFactory loggerFactory)
  12:     {
  13:         try
  14:         {
  15:             await _next(context);
  16:         }
  17:         catch (Exception ex)
  18:         {
  19:             loggerFactory.CreateLogger(_category).LogError(ex.Message);
  20:             throw;
  21:         }
  22:     }
  23: }

四、定义InterceptorAttribute

由于我们采用标注Attribute的方式,我们为这样的Attribute定义了一个名为InterceptorAttribute的基类。针对ErrorLogger的ErrorLoggerAttribute定义如下,它的核心在与需要实现抽象方法Use并利用作为参数的IInterceptorChainBuilder 注册对应的ErrorLogger。IInterceptorChainBuilder 中定义了一个泛型的方法使我们很容易地实现针对某个Interceptor类型的注册。该方法的第一个参数是整数,它决定注册的Interceptor在整个Interceptor有序列表中的位置。InterceptorAttribute中定义了对应的Order属性。如果注册Interceptor类型的构造还是具有不能通过依赖注入的参数,我们需要在调用Use方法的时候显式指定(比如category)。

   1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method, AllowMultiple = false)]
   2: public class ErrorLoggerAttribute : InterceptorAttribute
   3: {
   4:     private string _category;
   5:  
   6:     public ErrorLoggerAttribute(string category)
   7:     {
   8:         _category = category;
   9:     }
  10:     public override void Use(IInterceptorChainBuilder builder)
  11:     {
  12:         builder.Use<ErrorLogger>(this.Order, _category);
  13:     }
  14: }

InterceptorAttribute可以应用在类和方法上(我不赞成将它应用到接口上),在默认情况下它的AllowMultiple 属性为False。如果我们希望Interceptor链中可以包含多个相同类型的Interceptor,我们可以将AllowMultiple 属性设置为True。值得一提的是,在AllowMultiple 属性为False的情况下,如果类型和方法上都应用了同一个InterceptorAttribute,那么只会选择应用在方法上的那一个。在如下的代码中,我们将ErrorLoggerAttribute应用到总是会抛出异常的Invoke方法中,并且将日志类型设置为“App”。

   1: public interface IFoobarService
   2: {
   3:     void Invoke();
   4: }
   5:  
   6: public class FoobarService : IFoobarService
   7: {
   8:     [ErrorLogger("App")]
   9:     public void Invoke()
  10:     {
  11:         throw new InvalidOperationException("Manually thrown exception!");
  12:     }
  13: }

五、以DI的方式注入代理

我们依然会以DI的方式来使用上面定义的服务IFoobarService,但是毫无疑问,注入的对象必须是目标对象(FoobarService)的代理,我们注册的Interceptor才能生效,为了达到这个目的,我们需要使用如下这个IInterceptable<T>接口,它的Proxy属性为我们返回需要的代理对象。

   1: namespace Dora.Interception
   2: {
   3:     public interface IInterceptable<T> where T : class
   4:     {
   5:         T Proxy { get; }
   6:     }
   7: }

比如我们选在在MVC应用中将IFoobarService注入到Controller中,我们可以采用如下的定义方式。

   1: public class HomeController
   2: {
   3:     private IFoobarService _service;
   4:     public HomeController(IInterceptable<IFoobarService> interceptable)
   5:     {
   6:         _service = interceptable.Proxy;
   7:     }
   8:     [HttpGet("/")]
   9:     public string Index()
  10:     {
  11:         _service.Invoke();
  12:         return "Hello World";
  13:     }
  14: }

接下来我们来完成这个应用余下的部分。如下面的代码片段所示,我们在作为启动类Startup的ConfigureServicves方法中调用IServiceCollection的扩展方法AddInterception注册于Interception相关的服务。为了确定ErrorLogger是否将异常信息写入日志,我们在Main方法中添加了针对ConsoleLoggerProvider的注册,并选择只写入类型为“App”的日志。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .ConfigureLogging(factory=>factory.AddConsole((category, level)=>category == "App"))
   7:             .UseKestrel()
   8:             .UseStartup<Startup>()
   9:             .Build()
  10:             .Run();
  11:     }
  12: }
  13:  
  14: public class Startup
  15: {
  16:     public void ConfigureServices(IServiceCollection services)
  17:     {
  18:         services
  19:             .AddInterception()
  20:             .AddScoped<IFoobarService, FoobarService>()
  21:             .AddMvc();
  22:     }
  23:  
  24:     public void Configure(IApplicationBuilder app)
  25:     {
  26:         app.UseDeveloperExceptionPage()
  27:             .UseMvc();
  28:     }
  29: }

运行该应用后,如果我们利用浏览器访问该应用,由于我们注册了DeveloperExceptionPageMiddleware中间件,所以会出入如下图所示的错误页面。而服务端的控制台会显示记录下的错误日志。

六、如果你不喜欢IInterceptable<T>接口

Interception自身的特质决定我们只有注入目标对象的代理才能让注册的Interceptor被执行,这个问题我们是利用IInterceptable<T>接口来实现的,可能有人觉得这种方法不是很爽的话,我们还有更好的解决方案。我们先将HomeController写成正常的形式。

   1: public class HomeController
   2: {
   3:     private IFoobarService _service;
   4:     public HomeController(IFoobarService service)
   5:     {
   6:         _service = service;
   7:     }
   8:     [HttpGet("/")]
   9:     public string Index()
  10:     {
  11:         _service.Invoke();
  12:         return "Hello World";
  13:     }
  14: }

接下来我们需要在Startup的ConfigureServices方法调用ServiceCollection的ToInterceptable方法即可。

   1: public class Startup
   2: {
   3:     public void ConfigureServices(IServiceCollection services)
   4:     {
   5:         services
   6:             .AddInterception()
   7:             .AddScoped<IFoobarService, FoobarService>()
   8:             .AddMvc();
   9:         services.ToInterceptable();
  10:     }
  11:  
  12:     public void Configure(IApplicationBuilder app)
  13:     {
  14:         app.UseDeveloperExceptionPage()
  15:             .UseMvc();
  16:     }
  17: }

目前来说,如果采用这种方法,我们需要让注入的服务实现一个空的IInterceptable接口,因为我会利用它来确定某个对象是否需要封装成代理,将来我会将这个限制移除。

   1: public class FoobarService : IFoobarService, IInterceptable
   2: {
   3:     [ErrorLogger("App")]
   4:     public void Invoke()
   5:     {
   6:         throw new InvalidOperationException("Manually thrown exception!");
   7:     }
   8: }
作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech

NET Core写了一个轻量级的Interception框架[开源]的更多相关文章

  1. 为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]

    ASP.NET Core具有一个以ServiceCollection和ServiceProvider为核心的依赖注入框架,虽然这只是一个很轻量级的框架,但是在大部分情况下能够满足我们的需要.不过我觉得 ...

  2. 一个轻量级分布式RPC框架--NettyRpc

    1.背景 最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章<轻量级分布式 RPC 框架>,作者用Zookeeper.Netty和Spring写了一个轻量级的分布式RPC ...

  3. 一个轻量级分布式 RPC 框架 — NettyRpc

    原文出处: 阿凡卢 1.背景 最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章<轻量级分布式 RPC 框架>,作者用Zookeeper.Netty和Spring写了一个 ...

  4. 如何在Java生态圈选择一个轻量级的RESTful框架?

    在微服务流行的今天,我们会从纵向和横向分解代码的逻辑,将一些独立的无状态的代码单元实现为微服务,可以将它们发布到一些分布式计算单元或者Docker中,并在性能需要的时候及时地创建更多的服务单元.微服务 ...

  5. 写的一个轻量级javascript框架的设计模式

    公司一直使用jQuery框架,一些小的项目还是觉得jQuery框架太过于强大了,于是自己周末有空琢磨着写个自己的框架.谈到js的设计模式,不得不说说js的类继承机制,javascript不同于PHP可 ...

  6. PHP写的一个轻量级的DI容器类(转)

    理解什么是Di/IoC,依赖注入/控制反转.两者说的是一个东西,是当下流行的一种设计模式.大致的意思就是,准备一个盒子(容器),事先将项目中可能用到的类扔进去,在项目中直接从容器中拿,也就是避免了直接 ...

  7. C++11实现一个轻量级的AOP框架

    AOP介绍 AOP(Aspect-Oriented Programming,面向方面编程),可以解决面向对象编程中的一些问题,是OOP的一种有益补充.面向对象编程中的继承是一种从上而下的关系,不适合定 ...

  8. 介绍一个轻量级iOS安全框架:SSKeyChain

    SSKeyChains对苹果安全框架API进行了简单封装,支持对存储在钥匙串中密码.账户进行访问,包括读取.删除和设置.SSKeyChain的作者是大名鼎鼎的SSToolkit的作者samsoffes ...

  9. 一个轻量级iOS安全框架:SSKeyChain

    摘要 SSKeyChains对苹果安全框架API进行了简单封装,支持对存储在钥匙串中密码.账户进行访问,包括读取.删除和设置.SSKeyChain的作者是大名鼎鼎的SSToolkit的作者samsof ...

随机推荐

  1. Early Media and Music on Hold

    Early media refers to any media that is played to the initial caller’s phone before the remote party ...

  2. BZOJ-4819: 新生舞会(01分数规划+费用流)

    Description 学校组织了一次新生舞会,Cathy作为经验丰富的老学姐,负责为同学们安排舞伴.有n个男生和n个女生参加舞会 买一个男生和一个女生一起跳舞,互为舞伴.Cathy收集了这些同学之间 ...

  3. 如何卸载ubuntu软件

    你的硬盘空间已经不太足够了?如果你使用的是Ubuntu操作系统,你可能想知道如何能够卸载过时.无用的程序.有几种方法可以卸载程序,包括图形化方法和命令行方法.参考本指南,采用最适合你的方法卸载程序. ...

  4. 【LeetCode】040. Combination Sum II

    题目: Given a collection of candidate numbers (C) and a target number (T), find all unique combination ...

  5. [转]BX9054: 各浏览器对 document.execCommand 方法的首参数可选值范围存在差异

    作者:钱宝坤 标准参考 无. 问题描述 execCommand 方法通常用于控制可编辑的 IFRAME 内容,制作富文本编辑器. 但他现在为止还是非标准的,方法的首参数 Commmands 的可选值由 ...

  6. Python 静态方法和类方法的区别

    python staticmethod and classmethod Though classmethod and staticmethod are quite similar, there’s a ...

  7. POJ1144(割点入门题)

    Network Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 11378   Accepted: 5285 Descript ...

  8. POJ1860(ford判环)

    Currency Exchange Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 24243   Accepted: 881 ...

  9. Apache2.2安装图解

    Apache2.2安装图解 2010-12-14 15:32:44|  分类: 不学无术之杂 |  标签:安装  端口  httpd  apache2.2  服务器   |字号 订阅 Apache音译 ...

  10. 虚拟机出现ping DUP

    在主机的网络连接里,停用虚拟网卡vmnet1和vmnet8,再启用虚拟网卡vmnet1和vmnet8.