Dora.Interception,為.NET Core度身打造的AOP框架 [3]:多樣化攔截器應用方式
Dora.Interception,為.NET Core度身打造的AOP框架 [3]:多樣化攔截器應用方式
在《以約定的方式定義攔截器》中,我們通過對攔截器的介紹了Dora.Interception的兩種攔截機制,即針對介面的“例項攔截”針對虛方法的“型別攔截”。我們介紹了攔截器的本質以及基於約定的攔截器定義方式,接下來我們將著重關注攔截器的應用問題。
一、攔截器應用解決什麼問題
和有些AOP框架不同,Dora.Interception在設計刻意地將攔截器和攔截器應用刻意地分開。不僅如此,在攔截器和攔截器應用之間,我們還分離出“攔截器管道的構建”:
- 攔截器:旨在完成單一攔截功能的實現;
- 攔截器管道的構建:將多個攔截器按照指定的順序構建一個管道;
- 攔截器的應用:將構建的攔截器管道應用到被攔截的某個方法上。
二、IInterceptorChainBuilder
攔截器管道的構建由IInterceptorChainBuilder來完成,它類似於ASP.NET Core的IApplicationBuilder介面,後者利用註冊的中介軟體來構建一箇中間件管道,而IInterceptorChainBuilder則採用類似的方式將註冊的攔截器構建成一個攔截器管道。如下面的程式碼片段所示,我們利用Use方法將表示攔截器的InterceptorDelegate 的委託物件提供給IInterceptorChainBuilder,該方法的order引數表示提供的攔截器最終在攔截器鏈條上的位置。攔截器管道的構建最終由Build方法來完成,構建的管道也體現為一個InterceptorDelegate型別的委託。
public interface IInterceptorChainBuilder
{
InterceptorDelegate Build();
IInterceptorChainBuilder New();
IInterceptorChainBuilder Use(InterceptorDelegate interceptor, int order);
IServiceProvider ServiceProvider { get; }
}
由於Dora.Interception是為.NET Core度身定製的,而.NET Core總是離不開通過通過IServiceProvider表示的DI容器,所以我們將IServiceProvider整合到IInterceptorChainBuilder中,我們在構建攔截器管道過程中所需的任何一個依賴服務都可以利用它來提取。IInterceptorChainBuilder的New方法用來建立一個新的IInterceptorChainBuilder物件,當我們開始構建一個管道的時候需要呼叫此方法。
雖然Dora.Interception最終總是利用InterceptorDelegate物件來表示攔截器,但是我們推薦應用程式採用我們約定的方式將攔截器定義成一個POCO型別,所謂我們為IInterceptorChainBuilder定義瞭如下幾個擴充套件方法來註冊一次方式定義的攔截器型別。如果呼叫第一個和第三個Use方法提供攔截器型別(第二個Use方法直接提供的是攔截器物件),我們最終需要利用作為DI容器的IServiceProvider物件來建立對應的例項。如果建構函式中所有的引數都是預選註冊的服務,我們無需提供任何的引數,否則就需要利用arguments來提供它們。
public static class InterceptorChainBuilderExtensions
{
public static IInterceptorChainBuilder Use<TInterceptor>(this IInterceptorChainBuilder builder, int order, params object[] arguments);
public static IInterceptorChainBuilder Use(this IInterceptorChainBuilder builder, object interceptor, int order);
public static IInterceptorChainBuilder Use(this IInterceptorChainBuilder builder, Type interceptorType, int order, params object[] arguments);
}
三、IInterceptorProvider
IInterceptorChainBuilder僅僅是一個用來構建攔截器管道的工具而已,最終向它提供原材料(攔截器)的是一個IInterceptorProvider物件。一般來說一個攔截器型別對應一個IInterceptorProvider實現(也可可以是多個)。如下面的程式碼片段所示,IInterceptorProvider同樣定義了一個Use方法,該方法將上面這個IInterceptorChainBuilder作為引數。在具體的實現中,我們一般會呼叫IInterceptorChainBuilder的Use方法來註冊對應的攔截器型別。IInterceptorProvider還具有一個AllowMultiple屬性表示當前型別的多個攔截器例項能夠同時存在於管道中。
public interface IInterceptorProvider
{
void Use(IInterceptorChainBuilder builder);
bool AllowMultiple { get; }
}
IInterceptorProvider介面實際上體現了攔截器的註冊方法,因為所謂的攔截器註冊本質上體現為如何向IInterceptorChainBuilder提供攔截器的問題。由於標準Attribute是我們推薦的註冊方式,我們為它們定義瞭如下這個名為InterceptorAttribute的基類。InterceptorAttribute可以標註到型別、屬性和方法上,它的AllowMultiple屬性與標註到該Attribute上的AttributeUsageAttribute的AllowMultiple屬性一致,預設值為False。
[AttributeUsage((AttributeTargets) (AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Class), AllowMultiple=false)]
public abstract class InterceptorAttribute : Attribute, IInterceptorProvider,
{
public abstract void Use(IInterceptorChainBuilder builder);
public bool AllowMultiple { get; }
public int Order { get; set; }
}
比如在前面一章中,我們定義瞭如下這麼一個典型的Interceptor型別:
public class FoobarInterceptor
{
public IFoo Foo { get; }
public string Baz { get; }
public FoobarInterceptor(IFoo foo, string baz)
{
Foo = foo;
Baz = baz;
}
public async Task InvokeAsync(InvocationContext context, IBar bar)
{
await Foo.DoSomethingAsync();
await bar.DoSomethingAsync();
await context.ProceedAsync();
}
}
我們可以為它定義如下這麼一個型別為FoobarInterceptorAttribute 的IInterceptorProvider的實現。
[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method)]
public class FoobarInterceptorAttribute : InterceptorAttribute
{
public string Baz { get; }
public FoobarInterceptorAttribute(string baz) => Baz = baz;
public override void Use(IInterceptorChainBuilder builder) => builder.Use<FoobarInterceptor>(Order, Baz);
}
雖然Dora.Interception是將Interceptor和IInterceptorProvider區分開來,但是應用程式可以採用如下的方式將它們合二為一。其實將它們分而治之還有一個好處,那就是我可以為IInterceptorProvider起一個不同的名稱,比如第一篇演示的例項中我們將攔截器命名為CachingInterceptor,但是對應的IInterceptorProvider實現型別則定義成CacheReturnValueAttribute。還有另一個好處就是可以為同一個攔截器名型別定義多一個不同的IInterceptorProvider實現。
public class FoobarInterceptorAttribute : InterceptorAttribute
{
public string Baz { get; }
public FoobarInterceptorAttribute(string baz) => Baz = baz;
public async Task InvokeAsync(InvocationContext context, IFoo foo, IBar bar)
{
await foo.DoSomethingAsync();
await bar.DoSomethingAsync();
await context.ProceedAsync();
}
public override void Use(IInterceptorChainBuilder builder) => builder.Use(this, Order);
}
四、IInterceptorProviderResolver
最終針對攔截器的應用體現在IInterceptorProviderResolver物件上。IInterceptorProvider其實幫助我們解決了一個核心問題:提供具體的攔截器並將它存放到對應的位置(即在最終構建的攔截器管道中的Order)。那麼針對攔截器的應用最終體現為:針對一個型別或者其成員(方法或者屬性),能夠提供怎樣的IInterceptorProvider。如下面的程式碼片段所示,IInterceptorProviderResolver提供了三個方法來解析應用到型別、方法和屬性(Get、Set或者Both)的IInterceptorProvider。
public interface IInterceptorProviderResolver
{
bool? WillIntercept(Type targetType);
IInterceptorProvider[] GetInterceptorProvidersForMethod(Type targetType, MethodInfo targetMethod);
IInterceptorProvider[] GetInterceptorProvidersForProperty(Type targetType, PropertyInfo targetProperty, PropertyMethod getOrSet);
IInterceptorProvider[] GetInterceptorProvidersForType(Type targetType);
}
[Flags]
public enum PropertyMethod
{
Get = 1,
Set = 2,
Both = 3,
}
我們說標註Attribute僅僅體現為針對攔截器的一種註冊方式而已,因為在Dora.Interception我們為它定義瞭如下這麼一個AttributeInterceptorProviderResolver,它是預設註冊的。
internal class AttributeInterceptorProviderResolver : IInterceptorProviderResolver
{
public IInterceptorProvider[] GetInterceptorProvidersForMethod(Type targetType, MethodInfo method);
public IInterceptorProvider[] GetInterceptorProvidersForProperty(Type targetType, PropertyInfo property, PropertyMethod propertyMethod);
public IInterceptorProvider[] GetInterceptorProvidersForType(Type type);
public bool? WillIntercept(Type type);
public bool? WillIntercept(Type targetType, MethodInfo method);
public bool? WillIntercept(Type targetType, PropertyInfo property);
}
五、自行實現你需要的攔截器應用方式
如果我們覺得基於Attribute的實現不能滿足你的需求,只需要自行實現上面這個IInterceptorProviderResolver介面就可以了。比如我們可以定義如下這個“萬能”的IInterceptorProviderResolver實現,因為我將針對IInterceptorProvider物件與目標方法的匹配規則定義成一個Func<MethodInfo, bool>。
public class InterceptorRegistry : IInterceptorProviderResolver
{
private readonly IInterceptorProvider[] _empty = new IInterceptorProvider[0];
private readonly Dictionary<IInterceptorProvider, Func<MethodInfo, bool>> _policies = new Dictionary<IInterceptorProvider, Func<MethodInfo, bool>>();
public IInterceptorProvider[] GetInterceptorProvidersForMethod(Type targetType, MethodInfo targetMethod)
=> _policies.Where(it => it.Value(targetMethod)).Select(it => it.Key).ToArray();
public IInterceptorProvider[] GetInterceptorProvidersForProperty(Type targetType, PropertyInfo targetProperty, PropertyMethod getOrSet)
{
switch (getOrSet)
{
case PropertyMethod.Get:
return GetInterceptorProvidersForMethod(targetType, targetProperty.GetMethod);
case PropertyMethod.Set:
return GetInterceptorProvidersForMethod(targetType, targetProperty.SetMethod);
default:
return GetInterceptorProvidersForMethod(targetType, targetProperty.GetMethod)
.Union(GetInterceptorProvidersForMethod(targetType, targetProperty.SetMethod))
.ToArray();
}
}
public IInterceptorProvider[] GetInterceptorProvidersForType(Type targetType) => _empty;
public bool? WillIntercept(Type targetType)
{
if (targetType.GetCustomAttributes<NonInterceptableAttribute>().Any())
{
return false;
}
return null;
}
public InterceptorRegistry Add(IInterceptorProvider interceptorProvider, Func<MethodInfo, bool> filter)
{
_policies.Add(interceptorProvider, filter);
return this;
}
}
這樣的自定義IInterceptorProviderResolver(InterceptorRegistry )可以採用如下的方式進行註冊。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
var registry = new InterceptorRegistry()
.Add(new CacheReturnValueAttribute(), method => method.Name == "GetCurrentTime" && method.DeclaringType == typeof(SystemClock));
services.AddInterception(builder=>builder.InterceptorProviderResolvers.Add("policy",registry));
}
...
}
或者
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var registry = new InterceptorRegistry()
.Add(new CacheReturnValueAttribute(), method => method.Name == "GetCurrentTime" && method.DeclaringType == typeof(SystemClock));
return services.BuildInterceptableServiceProvider(builder=>builder.InterceptorProviderResolvers.Add("policy",registry));
}
}
通過自定義IInterceptorProviderResolver可以幫助我們實現任意形式的攔截器註冊方式,但是千萬不能濫用。我個人的觀點是:這種用於註冊攔截器的規則必需是明確的,我們必需非常確切地知道攔截器最終應用到了哪個方法上。如果定義的規則太過模糊,比如針對方法名稱進行註冊,那麼我們的攔截器極有可能應用到某個我們並不希望的方法上。
[1]:更加簡練的程式設計體驗
[2]:基於約定的攔截器定義方式
[3]:多樣性的攔截器應用方式
[4]:與依賴注入框架的深度整合
[5]:對攔截機制的靈活定製