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);
}
}
运行结果:
2. 定义拦截器
对于所有的AOP框架来说,多个拦截器最终会应用到某个方法上。这些拦截器按照指定的顺序构成一个管道,管道的另一端就是针对目标方法的调用。
2.1 拦截器约定
::: tip 自定义的拦截器需满足以下条件:
- 拦截器必须是一个可实例化的类型;
- 必须有一个公共构造函数,可以定义任意参数;
- 拦截操作定义在一个名为
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();
参考