Dora拦截器详解

大纲

1. Quick Start

Dora拦截器,为.NET Core量身定制的AOP框架。

我们使用“缓存”这个应用场景来演示如何使用Dora:我们创建一个缓存拦截器,并将其应用到某个方法上。缓存拦截器会将目标方法的返回值缓存起来。在缓存过期之前,提供相同参数列表的方法调用会直接返回缓存的数据,而无需执行目标方法。

1.1 Nuget包

Dora.Interception (=v3.0.0)

1.2 定义拦截器

作为Dora.Interception区别于其他AOP框架的最大特性,我们注册的拦截器类型无需实现某个预定义的接口,因为我们采用基于“约定”的拦截器定义方式。

public class CacheInterceptor
{
    private readonly ConcurrentDictionary<object, object> _cache;

    public CacheInterceptor()
    {
        _cache = new ConcurrentDictionary<object, object>();
    }

    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.TryAdd(key, context.ReturnValue);
        }
    }
}

按照约定,拦截器类型只需要定义成一个普通的“公共、实例”类型即可。拦截操作需要定义在约定的InvokeAsync方法中,该方法的返回类型为Task,并且包含一个InvocationContext类型的参数。

public class Cachekey
{
    public MethodBase Method { get; }
    public object[] InputArguments { get; }
    public Cachekey(MethodBase method, object[] arguments)
    {
        Method = method;
        InputArguments = arguments;
    }
    public override bool Equals(object obj)
    {
        if (!(obj is Cachekey another))
        {
            return false;
        }
        if (!Method.Equals(another.Method))
        {
            return false;
        }
        for (int index = 0; index < InputArguments.Length; index++)
        {
            var argument1 = InputArguments[index];
            var argument2 = another.InputArguments[index];
            if (argument1 == null && argument2 == null)
            {
                continue;
            }
            if (argument1 == null || argument2 == null)
            {
                return false;
            }
            if (!argument2.Equals(argument2))
            {
                return false;
            }
        }
        return true;
    }
    public override int GetHashCode()
    {
        int hashCode = Method.GetHashCode();
        foreach (var argument in InputArguments)
        {
            hashCode ^= argument.GetHashCode();
        }
        return hashCode;
    }
}

1.3 注册拦截器

在方法上标注特性是我们最常用的拦截器注册方式,为此我们定义CacheAttribute

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

在重写的Use方法中,我们只需要调用作为参数的IInterceptorChainBuilder对象的Use<TInterceptor>方法将指定的拦截器添加到拦截器链条(同一个方法上可能同时应用多个拦截器)。

1.4 使用拦截器

为了能够很直观地看到针对方法返回值的缓存,我们定义了如下这个表示系统时钟的ISystemClock的服务接口。

public interface ISystemClock
{
    DateTime GetCurrentTime();
}

public class SystemClock : ISystemClock
{
    [Cache]
    public DateTime GetCurrentTime() => DateTime.Now;
}

1.5 依赖注入

public static void Main()
{
    var clock = new ServiceCollection()
         .AddInterception() // 注册Dora.Interception本身的服务
         .AddSingletonInterceptable<ISystemClock, SystemClock>() // 注册可被拦截的服务
         .BuildServiceProvider()
         .GetRequiredService<ISystemClock>();
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine(clock.GetCurrentTime());
        Thread.Sleep(1000);
    }
}

运行结果:

result jpg

2. 定义拦截器

对于所有的AOP框架来说,多个拦截器最终会应用到某个方法上。这些拦截器按照指定的顺序构成一个管道,管道的另一端就是针对目标方法的调用。

2.1 拦截器约定

::: tip 自定义的拦截器需满足以下条件:

  1. 拦截器必须是一个可实例化的类型;
  2. 必须有一个公共构造函数,可以定义任意参数;
  3. 拦截操作定义在一个名为InvokeAsync的方法中,返回类型为Task,其中包含一个名为InvocationContext的参数。
    :::

2.2 InvocationContext

我们为整个拦截器管道定义了一个统一的执行上下文,并将其命名为InvocationContext。我们可以利用InvocationContext对象得到方法调用上下文的相关信息,其中包括两个方法(定义在接口和实现类型),目标对象、参数列表(含输入和输出参数)、返回值(可读写)。

public abstract class InvocationContext
{    
    public abstract MethodInfo Method { get; }
    public MethodInfo TargetMethod { get; }
    public abstract object Target { get; }
    public abstract object[] Arguments { get; }
    public abstract object ReturnValue { get; set; }  
    public abstract IDictionary<string, object> Properties { get; }

    public Task ProceedAsync();
}

属性

  • Method: 注册类型的方法(示例中的ISystemClock.GetCurrentTime()
  • TargetMethod: 目标对象的方法(示例中的SystemClock.GetCurrentTime()
  • Target: 目标对象(示例中的SystemClock
  • Arguments: 参数列表(含输入和输出参数)
  • ReturnValue: 返回值(可读写)
  • Properties: 自定义的属性容器,我们可以利用它来存放任意与当前方法调用上下文相关的信息

方法

  • ProceedAsync: 该方法会调用后续的拦截器。如果是最后一个,则调用目标方法。

2.3 注入依赖服务

拦截器底层是基于.Net Core的依赖注入服务的,所以可以在拦截器定义中注入需要的依赖服务。

public class FoobarInterceptor
{
    private readonly IFoo _foo;
    private readonly IBar _bar;

    public FoobarInterceptor(IFoo foo)
    {
        _foo = foo;
    }

    public async InvokeAsync(InvocationContext context, IBar bar)
    {
        await PreInvokeAsync();
        await context.ProceedAsync();
        await PostInvokeAsync();
    }
}

注入依赖服务共有两种方式:构造函数注入InvokeAsync方法注入

::: warning 构造函数注入和InvokeAsync方法注入的区别
拦截器本质上是一个Singleton服务,我们不应该将Scoped服务注入到它的构造函数中。如果具有针对Scoped服务注入的需要,我们应该将它注入到InvokeAsync方法中。
:::

3. 注册拦截器

Dora.Interception提供了基于特性基于策略两种注册拦截器的方式。

3.1 特性Atrribute

自定义的Attribute只需继承InterceptorAttribute即可。

public abstract class InterceptorAttribute : Attribute, IInterceptorProvider
{
    public int Order { get; set; }
    public bool AllowMultiple { get; }

    public abstract void Use(IInterceptorChainBuilder builder);
}
  • Order: 表示拦截器最终在管道中的位置。
  • AllowMultiple: 表示是否允许多个拦截器标记在同一方法上。默认false,即多个相同拦截器只执行一个。
  • Use: 把拦截器注册到管道调用链中。

扩展方法

public static class InterceptorChainBuilderExtensions
{
    public static IInterceptorChainBuilder Use<TInterceptor>(this IInterceptorChainBuilder builder, 
        int order, params object[] arguments);

    public static IInterceptorChainBuilder Use(this IInterceptorChainBuilder builder, 
        Type interceptorType, int order, params object[] arguments);

    public static IInterceptorChainBuilder Use(this IInterceptorChainBuilder builder, object interceptor, int order);
}
  • order: 指明拦截器调用的位置。
  • arguments: 如果构造函数的参数,依赖注入框架无法提供,则在这里指定。

3.2 策略Policy

策略这种方式的使用示例。

public static void Main(string[] args)
{
    new HostBuilder()
    .UseInterceptableServiceProvider(configure: Configure)
    .Build()
    .Run();
}

public static void Configure(InterceptionBuilder interceptionBuilder)
{
    interceptionBuilder.AddPolicy(policyBuilder => policyBuilder
                            // 注册CacheAttribute拦截器, order=1
                            .For<CacheAttribute>(order: 1, cache => cache
                                // 应用到SystemClock
                                .To<SystemClock>(target => target
                                    // 拦截方法GetCurrentTime
                                    .IncludeMethod(clock => clock.GetCurrentTime())
                                    .IncludeAllMembers() // 包含所有的方法
                                    .IncludeProperty(xxx) // 包含属性
                                    .ExcludeMethod(xxx) // 不拦截方法
                                    .ExcludeProperty(xxx); // 不拦截属性
                                )
                            )
                        )
}

4. 依赖注入

4.1 第一种方式

注册可被拦截的服务。

var services = new ServiceCollection()
         .AddInterception() // 注册Dora.Interception本身的服务
         .AddSingletonInterceptable<ISystemClock, SystemClock>(); // 注册可被拦截的服务

4.2 第二种方式

如果不想改变服务默认的注册方式,可以改用BuildInterceptableServiceProvider方法。

var services = new ServiceCollection()
    .AddSingleton<ISystemClock, SystemClock>()
    .BuildInterceptableServiceProvider();

BuildInterceptableServiceProvider方法内部会调用AddInterception

4.3 第三种方式

Dora.Interception作为第三方依赖注入框架,提供了IServiceProviderFactory接口的实现类,轻松实现与 .Net Core 依赖注入框架的整合。

public sealed class InterceptableServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{
    public InterceptableServiceProviderFactory(ServiceProviderOptions options, Action<InterceptionBuilder> configure);

    public IServiceCollection CreateBuilder(IServiceCollection services);
    public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder);
}

可以直接调用UseInterceptableServiceProvider这一扩展方法,完成针对InterceptableServiceProviderFactory的注册。

new HostBuilder()
    .UseInterceptableServiceProvider(configure: null)
    .Build()
    .Run();

参考

  1. AOP框架Dora.Interception 3.0 [1]: 编程体验
  2. AOP框架Dora.Interception 3.0 [2]: 实现原理
  3. AOP框架Dora.Interception 3.0 [3]: 拦截器设计
  4. AOP框架Dora.Interception 3.0 [4]: 基于特性的拦截器注册
  5. AOP框架Dora.Interception 3.0 [5]: 基于策略的拦截器注册
posted @ 2022-08-27 15:03  renzhsh  阅读(98)  评论(0编辑  收藏  举报