在 《不一樣的Interceptor》中我們著重介紹了Dora.Interception中最為核心的對象Interceptor,以及定義Interceptor類型的一些約定。由於Interceptor總是通過攔截某個方法的調用進而實現對前置或者後置操作的註入,所以我們定義的Interceptor類型... ...
在《不一樣的Interceptor》中我們著重介紹了Dora.Interception中最為核心的對象Interceptor,以及定義Interceptor類型的一些約定。由於Interceptor總是通過攔截某個方法的調用進而實現對前置或者後置操作的註入,所以我們定義的Interceptor類型總是需要與對應的目標方法進行映射。在預設的情況下,這種映射是通過在目標類型或者方法上標註特性的方式來實現的。對於任何一個Interceptor類型,我們總是需要為它定義一個對應的特性類型,這些特性具有一個共同的基類InterceptorAttribute。
目錄
一、InterceptorAttribute
二、如何定義和使用InterceptorAttribute
三、InterceptorAttribute的作用域
四、屏蔽某種類型的InterceptorAttribute
五、對其他註冊方式的支持
一、InterceptorAttribute
如下所示的是InterceptorAttribute特性的定義,我們可以看到它實現了一個名為IInterceptorProvider的介面,顧名思義,該介面表示為Dora.Interception的Interceptor Chain的構建系統提供單個Interceptor。昨天有人問我為什麼不將Interceptor直接定義成Attribute,那麼就可以直接標準在目標類型或其成員上了?對於這個問題我是這麼想的:作為一個攔截器,Interceptor只需要考慮如何實現其攔截操作就可以了,而對應的Attribute的職責是如何向Interceptor Chain構建系統提供對應的Interceptor,按照我們熟悉的“單一職責”的基本設計原則,兩者是應該分離的。從另一個角度來講,InterceptorAttribute僅僅體現了Interceptor的一種“註冊方式“,除了這種特性標準的註冊方式,Interceptor完全還可以採用其他的註冊方式,比如基於自定義映射規則,或者配置文件的方式。雖然在設計層面我將兩者嚴格地區分開來,但是最終用戶在定義Interceptor類型的時候是完全可以將兩者合二為一的,我們只需要將Interceptor同時定義成繼承InterceptorAttribute的特性類型就可以了。
IInterceptorProvider介面具有兩個成員,其中核心成員Use實現了針對Interceptor的註冊。至於另一個名為AllowMutiple的屬性,它表示由通過具有相同類型的InterceptorProvider提供的Interceptor是否可以同時應用到同一個方法上。
1 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method| AttributeTargets.Property, AllowMultiple = false)] 2 public abstract class InterceptorAttribute : Attribute, IInterceptorProvider 3 { 4 public int Order { get; set; } 5 public bool AllowMultiple {get;} 6 public abstract void Use(IInterceptorChainBuilder builder); 7 } 8 9 public interface IInterceptorProvider 10 { 11 void Use(IInterceptorChainBuilder builder); 12 bool AllowMultiple { get; } 13 }
IInterceptorProvider介面的Use方法具有一個類型為IInterceptorChainBuilder的參數,該介面表示一個用於構建Interceptor Chain的InterceptorChainBuilder對象,我們可以調用其Use方法向它提供一個Interceptor對象。該方法的第一個參數表示提供的Interceptor對象,而第二個參數(order)則表示這個Interceptor在最終構建的Interceptor Chain中所處的位置。除了這個Use方法,我們還額外定義了兩個同名的擴展方法,其中Use<T>是我們最為常用的。
1 public interface IInterceptorChainBuilder 2 { 4 IInterceptorChainBuilder Use(InterceptorDelegate interceptor, int order); 5 ...
7 } 8 9 public static class InterceptorChainBuilderExtensions 10 { 11 public static IInterceptorChainBuilder Use<TInterceptor>(this IInterceptorChainBuilder builder, int order, params object[] arguments); 12 public static IInterceptorChainBuilder Use(this IInterceptorChainBuilder builder, Type interceptorType, int order, params object[] arguments); 13 }
我們現在在回頭看看InterceptorAttribute類型,這個類型具有一個Order屬性正好對應於上面Use方法的order參數,而實現的AllowMultiple方法在預設的情況下與AttributeUsage.AllowMultiple屬性具有相同的值。所有派生於InterceptorAttribute的子類都需要重寫用於提供對應Interceptor的Use方法,一般來說我們只需要調用作為參數的IInterceptorChainBuilder對應的Use方法即可。在標註具有某個InterceptorAttribute的時候,我們可以按照如下的方式通過Order屬性控制Interceptor的執行順序:
1 public class Foobar 2 { 3 [Foo(Order = 1)] 4 [Bar(Order = 2)] 5 public virtual void Invoke() 6 {} 7 }
二、如何定義InterceptorAttribute
當我們為某個Interceptor類型定義對應InterceptorProvider特性的時候,只需要繼承InterceptorAttribute類型,並實現其Use方法即既可以,但是在調用IInterceptorChainBuilder的Use方法的時候針對參數的指定則取決於Interceptor類型構造函數的定義,以及針對DI的服務註冊。舉個簡單的例子,如下這個FoobarInterceptor的構造函數具有四個參數,除了第一個必需的參數由Interceptor的激活系統自行提供之外,其它的三個參數要麼通過DI系統的ServiceProvider來提供,要麼有對應的InterceptorProvdier來提供。
1 public class FoobarInterceptor 2 { 3 public FoobarInterceptor(InterceptDelegate next, IFoo foo, IBar bar, string baz); 4 public Task InvokeAsync(InvocationContext context); 5 }
假設前面兩個參數foo和bar由DI系統的ServiceProvider來提供,當我們為InterceptorProvider定義InterceptorAttribute的時候,實現的Use方法只需要提供baz參數的值就可以了,那麼我們可以採用如下的方式來定義這個InterceptorAttribute。
1 [AttributeUsage( AttributeTargets.Method| AttributeTargets.Class| AttributeTargets.Parameter, AllowMultiple = false )] 2 public class FoobarAttribute : InterceptorAttribute 3 { 4 public string Baz { get; } 5 6 public FoobarAttribute(string baz) 7 { 8 this.Baz = baz; 9 } 10 public override void Use(IInterceptorChainBuilder builder) 11 { 12 builder.Use<FoobarInterceptor>(this.Order, this.Baz); 13 } 14 }
假設我們不希望以DI的方式來提供foo和bar兩個參數,我們可以按照如下的方式在調用IInterceptorChainBuilder的Use方法是顯式提供這兩個參數的值。
1 [AttributeUsage( AttributeTargets.Method| AttributeTargets.Class| AttributeTargets.Property, AllowMultiple = false )] 2 public class FoobarAttribute : InterceptorAttribute 3 { 4 public string Baz { get; } 5 6 public FoobarAttribute(string baz) 7 { 8 this.Baz = baz; 9 } 10 public override void Use(IInterceptorChainBuilder builder) 11 { 12 builder.Use<FoobarInterceptor>(this.Order, this.Baz, new Bar(), new Foo()); 13 } 14 }
三、InterceptorAttribute的作用域
Dora.Interception支持標註在類型、方法和屬性上的InterceptorAttribute。對於應用在類型上的InterceptorAttribute特性,由它提供的Interceptor實際上是應用到該類型上的所有可以被攔截的方法上。如果InterceptorAttribute被標註到屬性成員上,意味著該屬性的Get和Set方法同時應用了對應的Interceptor。如果我們只需要將Interceptor應用到某個屬性的Get方法或者Set方法上,我們可以選擇將 對應的InterceptorAttribute單獨應用到Get或者Set方法上。除此之外Dora.Interception還支持繼承的InterceptorAttribute,也就是說標註到基類上的InterceptorAttribute會自動被子類繼承。
在解析InterceptorAttribute特性的時候,我特意屏蔽了應用在介面上的特性,我是這樣考慮的:介面是一個多方契約,它不應該考慮實現的細節,而基於AOP的攔截則屬於單方的實現行為,所以InterceptorAttribute是不應該標註在介面上。我知道很多AOP框架(比如Unity)是可以直接將Interceptor(CallHandler)應用在介面上的,但是我覺得這一點不妥。
值得一提的是InterceptorAttribute的AllowMultiple屬性,如果該屬性返回True,意味針對這個類型的所有特性標註都是有效的。如果我們希望某個InterceptorAttribute提供的Interceptor在最終的目標方法上只能執行一次,我們需要通過應用AttributeUsage特性並將其AllowMultiple設置為False。我們知道AttributeUsage的AllowMultiple屬性只能控制對應的特性在同一個目標成員上的標註次數,也就是說對於一個AllowMultiple設置為False的Attribute,我們可以同時將其標註到類型和它的成員上的。Dora.Interception對此作了相應的處理,確保只有更接近目標方法上的特性採用有效的。
以如下的定義為例,如果FoobarAttribute的AllowMultiple被設置為False,對應方法Foo,只有應用在它自身方法上的FoobarAttribute有效。對於Bar屬性的Get和Set方法,只有應用在其屬性上的FoobarAttribute有效。對於Baz屬性,應用在自身屬性上的FoobarAttribute只會應用到其Set方法上,至於其Get方法則會使用應用在自身方法上的FoobarAttribute。
1 [Foobar] 2 public class Demo 3 { 4 [Foobar] 5 public virtual void Foo() 6 { 7 Console.WriteLine("Demo.Invoke() is invoked"); 8 } 9 10 [Foobar] 11 public virtual Bar Bar {get;set;} 12 13 [Foobar] 14 public virtual Baz Baz { [Foobar]get; set; } 15 }
四、屏蔽某種類型的InterceptorAttribute
如果某種類型的InterceptorAttribute提供的Interceptor只適用於某個類型的大部分成員,我們可以選擇單獨將它標註到這些成員上。我們也可以採用排除,直接將該InterceptorAttribute標準到類型上,然後再不適用的類型成員上標註一個具有如下定義的NonInterceptableAttribute特性。當我們在使用NonInterceptableAttribute特性的時候,如果沒有指定任何參數,意味著目標類型或者類型成員(方法或者屬性)是不需要被攔截的。如果指定了IntercecptorProvider的類型,它只會屏蔽對應的IntercecptorProvider類型。
1 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method| AttributeTargets.Property)] 2 public class NonInterceptableAttribute : Attribute 3 { 4 public Type[] InterceptorProviderTypes { get; } 5 public NonInterceptableAttribute(params Type[] interceptorProviderTypes) 6 }
以如下的代碼片段為例,基類Foo上標註了兩個InterceptorAttribute(Interceptor1Attribute和Interceptor2Attribute),由於其子類Bar上標註了NonInterceptableAttribute,所以整個類型都是不需要被攔截的。至於另一個子類 Baz,它會繼承者兩個InterceptorAttribute,但是Invoke放上通過標註NonInterceptableAttribute屏蔽了Interceptor1Attribute,所以只有Interceptor2Attribute對它來說是有效的。
1 [Interceptor1] 2 [Interceptor2] 3 public class Foo 4 { 5 ... 6 } 7 8 [NonInterceptable] 9 public class Bar : Foo 10 { 11 [Interceptor1] 12 public virtual void Invoke(); 13 } 14 15 public class Baz : Foo 16 { 17 [NonInterceptable(typeof(Interceptor1Attribute))] 18 public virtual void Invoke(); 19 }
五、對其他註冊方式的支持
我在設計Dora.Interception的時候參考了很多主流的AOP框架,而我是Unity多年深度使用者,曾經多次研究過Unity.Interception的源代碼。我覺得很多的AOP框架都過於複雜,刻意地添加了一些我覺得不它適合的特性,所以我的Dora.Interception在很多方面實際上在做減法。在Interceptor的註冊方面,實際上在開發的時候是提供了基於MatchingRule的註冊方式(這也是參考了Unity.Interception),利用定義的各種MatchingRule,我們可以採用各種匹配模式(比如類型/方法/屬性名稱、命名空間、程式集名稱以及標註的Tag)將某種的Interceptor應用到滿足規則的類型或者方法上。但是根據我個人的使用經驗來看,由於這種匹配模式過於“模糊”,我們非常容易無疑之間就擴大了某種Interceptor的應用範圍。所以我們在發佈Dora.Interception的時候將這種註冊方式移除了,所以目前為止只支持特性標註這一種註冊方式。不過Dora.Interception在這個方面給出了擴展點,如果需要可以自行實現。也許在下一個版本,我會提供一些額外的註冊方式,但是一定會要求這樣的註冊方式是“精準的”和“明確的”。
Dora.Interception, 為.NET Core度身打造的AOP框架 [1]:全新的版本
Dora.Interception, 為.NET Core度身打造的AOP框架 [2]:不一樣的Interceptor定義方式
Dora.Interception, 為.NET Core度身打造的AOP框架 [3]:Interceptor的註冊