zoukankan      html  css  js  c++  java
  • Dora.Interception,为.NET Core度身打造的AOP框架 [1]:更加简练的编程体验

    很久之前开发了一个名为Dora.Interception的开源AOP框架(github地址:https://github.com/jiangjinnan/Dora,如果你觉得这个这框架还有那么一点价值,请不吝多点一颗星),最近对它作了一些改进(包括编程模式和性能,目前最新版本2.1.4)。一直以来我对软件设计秉承的一个理念就是:好的设计应该是简单的设计。和其他AOP框架相比,虽然Dora.Interception提供的编程模式已经显得足够简单,但是我觉得还应该再简单点,再简单点。这个新版本对拦截器的定义和应用提供了更加简单的定义方式,同时对扩展性方法作了较大的改进,接下来我们通过一个简单实例来体验一下。源代码从这里下载。

    一、定义拦截器类型

    Dora.Interception中的拦截器类型不需要实现任何的接口或者继承任何的基类,因为我们采用“基于约定”的设计方案。由于Dora.Interception是建立在.NET Core的依赖注入框架之上,所以我们可以将任意依赖的服务直接注入到定义的截器类型中。接下来我们将定义一个名为CacheInterceptor的拦截器来实现针对方法返回值的缓存。由于缓存的内容是某个方法的返回值,所以我们将方法和参数列表作为缓存的Key,这个Key由如下这个CacheKey来表示(完整定义请参阅源代码)。

    复制代码
    public class CacheKey
    {
        public MethodBase Method { get; }
        public object[] InputArguments { get; }
    
        public CacheKey(MethodBase method, object[] arguments)
        {
            this.Method = method;
            this.InputArguments = arguments;
        }
        public override bool Equals(object obj);public override int GetHashCode();
    }
    复制代码

    我们直接利用ASP.NET Core基于内存的缓存框架来对方法返回值实施缓存,所以我们直接将IMemoryCache服务和对应的Options以如下的方式注入到CacheInterceptor的构造函数中。具体的拦截操作实现在按照约定定义的InvokeAsync方法中,我们可以利用作为输入参数的InvocationContext 对象得到当前方法调用的所有上下文信息,也可以直接通过它的ReturnValue设置方法的返回值。在如下所示的代码片段中,我们正是利用这个InvocationContext对象得到表示当前调用方法的MethodInfo对象和输入参数,并以它们创建出CacheKey对象来操作缓存。

    复制代码
    public class CacheInterceptor
    {
        private readonly IMemoryCache _cache;
        private readonly MemoryCacheEntryOptions _options;
        public CacheInterceptor(IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
        {
            _cache = cache;
            _options = optionsAccessor.Value;
        }
    
        public async Task InvokeAsync(InvocationContext context)
        {
            var key = new CacheKey(context.Method, context.Arguments);
            if (_cache.TryGetValue(key, out object value))
            {
                context.ReturnValue = value;
            }
            else
            {
                await context.ProceedAsync();
                _cache.Set(key, context.ReturnValue, _options);
            }
        }
    }
    复制代码

    对于一个拦截器对象来说,当调用被其拦截之后,需要由它自己来决定是否需要继续后续的调用,在新的版本中,我们采用直接调用InvocationContext的ProceedAsync方法的方式来达到这个目的。上面这个CacheInterceptor类型采用构造器注入的方式来注入依赖的服务,实际上我们还具有更加简单的方案,那就是采用如下的方式直接将依赖服务注入到InvokeAsync方法中。

    复制代码
    public class CacheInterceptor
    {
        public async Task InvokeAsync(InvocationContext context, 
    IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor
    )
        {
            var key = new CacheKey(context.Method, context.Arguments);
            if (cache.TryGetValue(key, out object value))
            {
                context.ReturnValue = value;
            }
            else
            {
                await context.ProceedAsync();
                cache.Set(key, context.ReturnValue, optionsAccessor.Value);
            }
        }
    }
    复制代码

    二、应用拦截器

    所谓的Interceptor应用就是如何将Interceptor应用到定义在某个类型上的某个方法的过程。Dora.Interception默认提供了多种注册方式,最为常用的莫过于采用Attribute标注的方式来注册Interceptor。如果需要采用这种方式来注册CacheInterceptor,我们需要采用如下的方式为Interceptor类型定义对应的CacheReturnValueAttribute 类型。CacheReturnValueAttribute 派生于抽象类InterceptorAttribute,在重写的Use方法中,它调用作为参数的IInterceptorChainBuilder 对象的Use方法将CacheInterceptor添加到Interceptor管道中,传入的参数(Order)代表Interceptor在管道中的位置。

    [AttributeUsage(AttributeTargets.Method)]
    public class CacheReturnValueAttribute : InterceptorAttribute
    {
        public override void Use(IInterceptorChainBuilder builder) => builder.Use<CacheInterceptor>(Order);
    }

    Dora.Interception刻意地将Interceptor和对应的Attribute区分对象,因为我们认为后者仅仅是Interceptor的一种“注册方式'’而已。如果我们希望将二者合一,我们可以采用如下的定义方式。

    复制代码
    public class CacheInterceptorAttribute : InterceptorAttribute
    {
        public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
        {
            var key = new CacheKey(context.Method, context.Arguments);
            if (cache.TryGetValue(key, out object value))
            {
                context.ReturnValue = value;
            }
            else
            {
                await context.ProceedAsync();
                cache.Set(key, context.ReturnValue, optionsAccessor.Value);
            }
        }
        public override void Use(IInterceptorChainBuilder builder) => builder.Use(this, Order);
    }
    复制代码

    为了演示CacheInterceptor针对目标返回值的缓存,我们定义了如下这个标识“系统时钟”的ISystemClock服务,它的GetCurrentTime方法返回当前的时间。为了验证基于参数的缓存,我们为该方法定义了一个表示事件类型(Local或者UTC)的参数。上面定义的CacheReturnValueAttribute标注在实现类型的GetCurrentTime方法上。很多AOP框架都支持将Interceptor直接应用到服务接口上,但我个人觉得这是不对的,因为接口表示的是双边契约,Interceptor体现的是单边的行为,所以Interceptor是不应该应用到接口上。

    复制代码
    public interface ISystemClock
    {
        DateTime GetCurrentTime(DateTimeKind dateTimeKind);
    }
    
    public class DefaultSystemClock : ISystemClock
    {
        [CacheReturnValue]
        public DateTime GetCurrentTime(DateTimeKind dateTimeKind)
        => dateTimeKind == DateTimeKind.Utc
                ? DateTime.UtcNow
                : DateTime.Now;
    }
    复制代码

    三、你的代码不需要任何改变

    将Dora.Interception引入你的应用完全不会影响你现有的代码,比如在消费ISystemClock服务的时候完全不用考虑CacheInterceptor的存在。如下所示的就是典型地在Controller中以注入形式消费服务的编程模式。

    复制代码
    public class HomeController: Controller
    {
        private readonly ISystemClock _clock;   
        public HomeController(
    ISystemClock clock
    )  => _clock = clock;
    
        [HttpGet("/")]
        public async Task Index()
        {
            async Task<string[]> GetTimesAsync()
            {
                var times = new string[6];
                for (int index = 0; index < 3; index++)
                {
                    times[index] = $"Local: {_clock.GetCurrentTime(DateTimeKind.Local)}";
                    await Task.Delay(1000);
                }
    
                for (int index = 3; index < 6; index++)
                {
                    times[index] = $"UTC: {_clock.GetCurrentTime(DateTimeKind.Utc)}";
                    await Task.Delay(1000);
                }
                return times;
            }
    
            var currentTimes = await GetTimesAsync();
            var list = string.Join("", currentTimes.Select(it => $"<li>{it}</li>"));
            Response.ContentType = "text/html";
            await Response.WriteAsync(
                @"<html>
                    <body>
                        <ul>" + list +
                        @"</ul>
                    </body>
                </html>");   
        }
    }
    复制代码

    我们唯一需要做的就是在注册Startup类型的ConfigureServices方法中调用IServiceCollection的扩展方法BuildInterceptableServiceProvider方法创建并返回一个IServiceProvider,后者能够帮助我们创建出能够被拦截的服务实例。BuildInterceptableServiceProvider方法提供的是一个InterceptableServiceProvider对象,InterceptableServiceProvider与目前.NET Core DI框架基本上是一致的,我仅仅对它作了一些微小的改动。

    复制代码
    public class Startup
    {
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services
                .AddSingleton<ISystemClock, DefaultSystemClock>()
                .AddMemoryCache()
                .AddMvc();
            return services.BuildInterceptableServiceProvider();
        }
        public void Configure(IApplicationBuilder app) => app.UseDeveloperExceptionPage().UseMvc();
    }
    复制代码

    如果运行上面这个简单的ASP.NET Core MVC应用,浏览器将会呈现出如下所示的输出结果。由于SystemClock的GetCurrentTime方法的返回值被缓存了,所以针对相同参数返回的时间是相同的。

    image

    [1]:更加简练的编程体验
    [2]:基于约定的拦截器定义方式
    [3]:多样性的拦截器应用方式
    [4]:与依赖注入框架的深度整合
    [5]:对拦截机制的灵活定制

    作者:蒋金楠
    微信公众账号:大内老A
    微博:www.weibo.com/artech
    如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
     
    ******转载:https://www.cnblogs.com/artech/p/dora-interception-01.html
  • 相关阅读:
    react 学习
    redux saga学习
    Power BI连接至Amazon Redshift
    php时间日期
    layui select 禁止点击
    微信小程序二维码是无法识别二维码跳转到小程序
    JSON字符串与JSON对象的区别
    前端切图要选择png和jpg呢?
    @media媒体查询
    TortoiseGit revert failed
  • 原文地址:https://www.cnblogs.com/linybo/p/10052977.html
Copyright © 2011-2022 走看看