热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

全新升级的AOP框架Dora.Interception[1]:编程体验

多年之前利用ILEmit写了一个名为Dora.Interception(github地址,觉得不错不妨给一颗星)的AOP框架。前几天利用Roslyn的SourceGenerator
多年之前利用IL Emit写了一个名为Dora.Interception(github地址,觉得不错不妨给一颗星)的AOP框架。前几天利用Roslyn的Source Generator对自己为公司写的一个GraphQL框架进行改造,性能得到显著的提高,觉得类似的机制同样可以用在AOP框架上,实验证明这样的实现方式不仅仅极大地改善性能(包括执行耗时和GC内存分配),而且让很多的功能特性变得简单了很多

多年之前利用IL Emit写了一个名为Dora.Interception(github地址,觉得不错不妨给一颗星)的AOP框架。前几天利用Roslyn的Source Generator对自己为公司写的一个GraphQL框架进行改造,性能得到显著的提高,觉得类似的机制同样可以用在AOP框架上,实验证明这样的实现方式不仅仅极大地改善性能(包括执行耗时和GC内存分配),而且让很多的功能特性变得简单了很多。这并不是说IL Emit性能不好(其实恰好相反),而是因为这样的实现太复杂,面向IL编程比写汇编差不多。由于AOP拦截机制涉及的场景很多(比如异步等待、泛型类型和泛型方法、按地址传递参数等等),希望完全利用IL Emit高效地实现所有的功能特性确实很难,但是从C#代码的层面去考虑就简单多了。(拙著《ASP.NET Core 6框架揭秘》于日前上市,加入读者群享6折优惠)

目录
一、Dora.Interception的设计特点
二、基于约定的拦截器定义
三、基于特性的拦截器注册方式
四、基于表达式的拦截器注册方式
五、更好的拦截器定义方式
六、方法注入
七、拦截的屏蔽
八、在ASP.NET Core程序中的应用

一、Dora.Interception的设计特点

彻底改造升级后的Dora.Interception直接根据.NET 6开发,不再支持之前.NET (Core)版本。和之前一样,Dora.Interception的定位是一款轻量级的AOP框架,同样建立在.NET的依赖注入框架上,可拦截的对象必需由依赖注入容器来提供。

除了性能的提升和保持低侵入性,Dora.Interception在编程方式上于其他所有的AOP框架都不太相同。在拦截器的定义上,我们并没有提供接口和基类来约束拦截方法的实现,而是采用“基于约定”的编程模式将拦截器定义成一个普通的类,拦截方法上可以任意注入依赖的对象。

在如何应用定义的拦截器方面,我们提供了常见的“特性标注”的编程方式将拦截器与目标类型、方法和属性建立关联,我们还提供了一种基于“表达式”的拦截器应用方式。Dora.Interception主张将拦截器“精准”地应用到具体的目标方法上,所以提供的这两种方式针对拦截器的应用都是很“明确的”。如果希望更加灵活的拦截器应用方式,通过提供的扩展可以***发挥。

接下来我们通过一个简单实例来演示一下Dora.Interception如何使用。在这个实例中,我们利用AOP的方式来缓存某个方法的结果,我们希望达到的效果很简单:目标方法将返回值根据参数列表进行缓存,以避免针对方法的重复执行。

二、基于约定的拦截器定义

我们创建一个普通的控制台程序,并添加如下两个NuGet包的引用。前者正是提供Dora.Interception框架的NuGet包,后者提供的基于内存缓存帮助我们缓存方法返回值。

  • Dora.Interception
  • Microsoft.Extensions.Caching.Memory

由于方法的返回值必须针对输入参数进行缓存,所以我们定义了如下这个类型Key作为缓存的键。作为缓存键的Key对象是对作为目标方法的MethodInfo对象和作为参数列表的对象数组的封装。

internal class Key : IEquatable
{
    public Key(MethodInfo method, IEnumerable<object> arguments)
    {
        Method = method;
        Arguments = arguments.ToArray();
    }

    public MethodInfo Method { get; }
    public object[] Arguments { get; }
    public bool Equals(Key? other)
    {
        if (other is null) return false;
        if (Method != other.Method) return false;
        if (Arguments.Length != other.Arguments.Length) return false;
        for (int index = 0; index if (!Arguments[index].Equals(other.Arguments[index]))
            {
                return false;
            }
        }
        return true;
    }
    public override int GetHashCode()
    {
        var hashCode = new HashCode();
        hashCode.Add(Method);
        for (int index = 0; index return hashCode.ToHashCode();
    }
    public override bool Equals(object? obj) => obj is Key key && key.Equals(this);
}

如下所示的就是用来缓存目标方法返回值的拦截器类型CachingInterceptor的定义。正如上面所示,Dora.Interception提供的是“基于约定”的编程方式。这意味着作为拦截器的类型不需要实现既定的接口或者继承既定的基类,它仅仅是一个普通的公共实例类型。由于Dora.Interception建立在依赖注入框架之上,所以我们可以在构造函数中注入依赖的对象,在这里我们就注入了用来缓存返回值的IMemoryCache 对象。

public class CachingInterceptor
{
    private readonly IMemoryCache _cache;
    public CachingInterceptor(IMemoryCache cache) => _cache = cache;

    public async ValueTask InvokeAsync(InvocationContext invocationContext)
    {
        var method = invocationContext.MethodInfo;
        var arguments = Enumerable.Range(0, method.GetParameters().Length).Select(index => invocationContext.GetArgument<object>(index));
        var key = new Key(method, arguments);

        if (_cache.TryGetValue<object>(key, out var value))
        {
            invocationContext.SetReturnValue(value);
            return;
        }
        await invocationContext.ProceedAsync();
        _cache.Set(key, invocationContext.GetReturnValue<object>());
    }
}

具体的“切面(Aspect)”逻辑实现在一个面向约定的InvokeAsync方法中,该方法只需要定义成返回类型为ValueTask的公共实例方法即可。InvokeAsync方法提供的InvocationContext 对象是针对当前方法调用的上下文,我们利用其MethodInfo属性得到代表目标方法的MethodInfo对象,调用泛型方法GetArgument根据序号得到传入的参数。在利用它们生成代码缓存键的Key对象之后,我们利用构造函数中注入的IMemoryCache 对象确定是否存在缓存的返回值。如果存在,我们直接调用InvocationContext 对象的SetReturnValue方法将它设置为方法返回值,并直接“短路”返回,目标方法将不再执行。

如果返回值尚未被缓存,我们调用InvocationContext 对象的ProceedAsync方法,该方法会帮助我们调用后续的拦截器或者目标方法。在此之后我们利用上下文的SetReturnValue方法将返回值提取出来进行缓存就可以了。

三、基于特性的拦截器注册方式

拦截器最终需要应用到某个具体的方法上。为了能够看到上面定义的CachingInterceptor针对方法返回值缓存功能,我们定义了如下这个用来提供系统时间戳的SystemTimeProvider服务类型和对应的接口ISystemTimeProvider,定义的GetCurrentTime方法根据作为参数的DateTimeKind枚举返回当前时间。实现在SystemTimeProvider中的GetCurrentTime方法上利用预定义的InterceptorAttribute特性将上面定义的CachingInterceptor拦截器应用到目标方法上,该特性提供的Order属性用来控制应用的多个拦截器的执行顺序。

public interface ISystemTimeProvider { DateTime GetCurrentTime(DateTimeKind kind); }

public class SystemTimeProvider : ISystemTimeProvider { [Interceptor(typeof(CachingInterceptor),Order = 1)] public virtual DateTime GetCurrentTime(DateTimeKind kind) => kind switch { DateTimeKind.Utc => DateTime.UtcNow, _ => DateTime.Now }; }

虽然大部分AOP框架都支持将拦截器应用到接口上,但是Dora.Interception倾向于避免这样做,因为接口是服务消费的契约,面向切面的横切(Crosscutting)功能体现的是服务实现的内部行为,所以拦截器应该应用到实现类型上。如果你一定要做么做,只能利用提供的扩展点来实现,实现方式其实也很简单。

Dora.Interception直接利用依赖注入容器来提供可被拦截的实例。如下面的代码片段所示,我们创建了一个ServiceCollection对象并完成必要的服务注册,最终调用BuildInterceptableServiceProvider扩展方法得到作为依赖注入容器的IServiceProvider对象。

var timeProvider = new ServiceCollection()
    .AddMemoryCache()
    .AddSingleton()
    .AddSingleton()
    .BuildInterceptableServiceProvider()
    .GetRequiredService();

Console.WriteLine("Utc time:");
for (int index = 0; index <5; index++)
{
    Console.WriteLine($"{timeProvider.GetCurrentTime(DateTimeKind.Utc)}[{DateTime.UtcNow}]");
    await Task.Delay(1000);
}


Console.WriteLine("Utc time:");
for (int index = 0; index <5; index++)
{
    Console.WriteLine($"{timeProvider.GetCurrentTime(DateTimeKind.Local)}[{DateTime.Now}]");
    await Task.Delay(1000);
}

在利用BuildInterceptableServiceProvider对象得到用于提供当前时间戳的ISystemTimeProvider服务实例,并在控制上以UTC和本地时间的形式输出时间戳。由于输出的间隔被设置为1秒,如果方法的返回值被缓存,那么输出的时间是相同的,下图所示的输出结果体现了这一点(源代码)。

image

四、基于Lambda表达式的拦截器注册方式

如果拦截器应用的目标类型是由自己定义的,我们可以在其类型或成员上标注InterceptorAttribute特性来应用对应的拦截器。如果对那个的程序集是由第三方提供的呢?此时我们可以采用提供的第二种基于表达式的拦截器应用方式。这里的拦截器是一个调用目标类型某个方法或者提取某个属性的Lambda表达式,我们采用这种强类型的编程方式得到目标方法,并提升编程体验。对于我们演示的实例来说,拦截器最终应用到SystemTimeProvider的GetCurrentTime方法上,所以我们可以按照如下的形式来代替标注在该方法上的InterceptorAttribute特性(源代码)。

var timeProvider = new ServiceCollection()
    .AddMemoryCache()
    .AddSingleton()
    .AddSingleton()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors))
    .GetRequiredService();

static void RegisterInterceptors(IInterceptorRegistry registry)
{
    registry.For().ToMethod(1, it => it.GetCurrentTime(default));
}

五、更好的拦截器定义方式

全新的Dora.Interception在提升性能上做了很多考量。从上面定义的CachingInterceptor可以看出,作为方法调用上下文的InvocationContext类型提供的大部分方法都是泛型方法,其目的就是避免装箱带来的内存分配。但是CachingInterceptor为了适应所有方法,只能将参数和返回值转换成object对象,所以这样会代码一些性能损失。为了解决这个问题,我们可以针对参数的个数相应的泛型拦截器。比如针对单一参数方法的拦截器就可以定义成如下的形式,我们不仅可以直接使用 Tuple元组作为缓存的Key,还可以直接调用泛型的GetArgument方法和SetReturnValue提起参数和设置返回值。

public class CachingInterceptor
{
    private readonly IMemoryCache _cache;
    public CachingInterceptor(IMemoryCache cache) => _cache = cache;

    public async ValueTask InvokeAsync(InvocationContext invocationContext)
    {
        var key = new Tuple(invocationContext.MethodInfo, invocationContext.GetArgument(0));
        if (_cache.TryGetValue(key, out var value))
        {
            invocationContext.SetReturnValue(value);
            return;
        }

        await invocationContext.ProceedAsync();
        _cache.Set(key, invocationContext.GetReturnValue());
    }
}

具体的参数类型只需要按照如下的方式在应用拦截器的时候指定就可以了(源代码)。

public class SystemTimeProvider : ISystemTimeProvider
{
    [Interceptor(typeof(CachingInterceptor), Order = 1)]
    public virtual DateTime GetCurrentTime(DateTimeKind kind) => kind switch
    {
        DateTimeKind.Utc => DateTime.UtcNow,
        _ => DateTime.Now
    };
}

六、方法注入

拦截器定义的时候可以在构造函数中注入依赖对象,其实更方便不是采用构造函数注入,而是采用方法注入,也就是直接将对象注入到InvokeAsync方法中。由于拦截器对象具有全局生命周期(从创建到应用关闭),所以Scoped服务不能注入到构造函数中,此时只能采用方法注入,因为方法中注入的对象是在方法调用时实时提供的。上面定义的拦截器类型改写成如下的形式(源代码)。

public class CachingInterceptor
{
    public async ValueTask InvokeAsync(InvocationContext invocationContext, IMemoryCache cache)
    {
        var key = new Tuple(invocationContext.MethodInfo, invocationContext.GetArgument(0));
        if (cache.TryGetValue(key, out var value))
        {
            invocationContext.SetReturnValue(value);
            return;
        }

        await invocationContext.ProceedAsync();
        cache.Set(key, invocationContext.GetReturnValue());
    }
}

七、拦截的屏蔽

除了“精准地”将某个拦截器应用到目标方法上,我们也可以采用“排除法”先将拦截器批量应用到一组候选的方法上(比如应用到某个类型设置是程序集上),然后将某些不需要甚至不能被拦截的方法排除掉。此外我们使用这种机制避免某些不能被拦截(比如在一个循环中重复调用)的方法被错误地与某些拦截器进行映射。针对拦截的屏蔽也提供了两种编程方式,一种方式就是在类型、方法或者属性上直接标注NonInterceptableAttribute特性。由于针对拦截的屏蔽具有最高优先级,如果我们按照如下的方式在SystemTimeProvider类型上标注NonInterceptableAttribute特性,针对该类型的所有方法的调用将不会被拦截(源代码)。

[NonInterceptable]
public class SystemTimeProvider : ISystemTimeProvider
{
    [Interceptor(typeof(CachingInterceptor), Order = 1)]
    public virtual DateTime GetCurrentTime(DateTimeKind kind) => kind switch
    {
        DateTimeKind.Utc => DateTime.UtcNow,
        _ => DateTime.Now
    };
}

我们也可以采用如下的方式调用SuppressType方法以表达式的方式提供需要屏蔽的方式。除了这个方法,IInterceptorRegistry接口还提供了其他方法,我们会在后续的内容进行系统介绍。

var timeProvider = new ServiceCollection()
    .AddMemoryCache()
    .AddSingleton()
    .AddSingleton()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors))
    .GetRequiredService();

...

static void RegisterInterceptors(IInterceptorRegistry registry) => registry.SupressType();

八、在ASP.NET Core程序中的应用

由于ASP.NET Core框架建立在依赖注入框架之上,Dora.Interception针对方法的拦截也是通过动态改变服务注册的方式实现的,所以Dora.Interception在ASP.NET Core的应用更加自然。现在我们将上面定义的ISystemTimeProvider/SystemTimeProvider服务应用到如下这个HomeController中。两个采用路由路径“/local”和“utc”的Action方法会利用注入的ISystemTimeProvider对象返回当前时间。为了检验返回的时间是否被缓存,方法还会返回当前的真实时间戳

public class HomeController
{
    [HttpGet("/local")]
    public string GetLocalTime([FromServices] ISystemTimeProvider provider) => $"{provider.GetCurrentTime(DateTimeKind.Local)}[{DateTime.Now}]";

    [HttpGet("/utc")]
    public string GetUtcTime([FromServices] ISystemTimeProvider provider) => $"{provider.GetCurrentTime(DateTimeKind.Utc)}[{DateTime.UtcNow}]";
}

ASP.NET Core针对Dora.Interception的整合是通过调用IHostBuilder的UseInterception扩展方法实现的,该扩展方法由“Dora.Interception.AspNetCore”提供(源代码)。

using App;

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseInterception();
builder.Services
    .AddHttpContextAccessor()
    .AddMemoryCache()
    .AddSingleton()
    .AddControllers();
var app = builder.Build();
app
    .UseRouting()
    .UseEndpoints(endpint => endpint.MapControllers());
app.Run();

程序启动后,我们请求路径“local”和“utc”得到的时间戳都将被缓存起来,如下的输出结果体现了这一点(源代码)。

image

全新升级的AOP框架Dora.Interception[1]: 编程体验
全新升级的AOP框架Dora.Interception[2]: 基于约定的拦截器定义方式
全新升级的AOP框架Dora.Interception[3]: 基于“特性标注”的拦截器注册方式
全新升级的AOP框架Dora.Interception[4]: 基于“Lambda表达式”的拦截器注册方式
全新升级的AOP框架Dora.Interception[5]: 实现任意的拦截器注册方式
全新升级的AOP框架Dora.Interception[6]: 框架设计和实现原理


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 标题: ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
author-avatar
飞鱼
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有