在Dora.Interception中按照約定方式定義的攔截器可以采用多種方式注冊到目標方法上。本篇文章介紹最常用的基于“特性標注”的攔截器注冊方式,下一篇會介紹另一種基于(Lambda)表達式的注冊方式:全新升級的AOP框架Dora.Interception[4]: 基于表達式的攔截器注冊。
目錄
一、InterceptorAttribute 特性
二、指定構造攔截器的參數列表
三、將攔截器類型定義成特性
四、合法性檢驗
五、針對類型、屬性的標注
六、攔截的屏蔽
一、InterceptorAttribute 特性
攔截器類型可以利用如下這個InterceptorAttribute特性應用到標注的類型、屬性和方法上。除了通過Interceptor屬性指定攔截器類型之外,我們還可以利用Order屬性控制攔截器的執行順序,該屬性默認值為0。該特性的Arguments用來提供構造攔截器對象的參數。
[AttributeUsage(AttributeTargets.Class?|?AttributeTargets.Method?|?AttributeTargets.Property,?AllowMultiple?=?true,?Inherited?=?false)]
public?class?InterceptorAttribute?:?Attribute
{public?Type?Interceptor?{?get;?}public?object[]?Arguments?{?get;?}public?int?Order?{?get;?set;?}public?InterceptorAttribute(params?object[]?arguments)?:public?InterceptorAttribute(Type??interceptor,?params?object[]?arguments);
}
二、指定構造攔截器的參數列表
攔截器對象是通過依賴注入容器提供的,容器能夠自動提供注入到構造函數中對象。如果構造函數包含額外的參數,對應的參數值就需要利用InterceptorAttribute 特性的Arguments屬性來提供,此屬性由構造函數的arguments參數提供。
public?class?FoobarInterceptor
{public?string?Name?{?get;??}public?FoobarInterceptor(string?name,?IFoobar?foobar){Name?=?name;Debug.Assert(foobar?is?not?null);}public?ValueTask?InvokeAsync(InvocationContext?invocationContext){Console.WriteLine($"FoobarInterceptor?'{Name}'?is?invoked.");return?invocationContext.ProceedAsync();}
}public?interface?IFoobar?{?}
public?class?Foobar?:?IFoobar?{?}
對于如上這個攔截器類型FoobarInterceptor,其構造函數定義了一個字符串的參數name用來指定攔截器的名稱,當我利用InterceptorAttribute 特性將此攔截器應用到Invoker類型的Invoke1和Invoke2方法上是,就需要按照如下的方式指定具體的名稱(Interceptor1和Interceptor2)。
public?class?Invoker
{[FoobarInterceptor("Interceptor1")]public?virtual?void?Invoke1()?=>?Console.WriteLine("Invoker.Invoke1()");[FoobarInterceptor("Interceptor2")]public?virtual?void?Invoke2()?=>?Console.WriteLine("Invoker.Invoke2()");
}
我們按照如下的方式調用Invoker對象的Invoke1和Invoke2方法。
var?invoker?=?new?ServiceCollection().AddSingleton<Invoker>().AddSingleton<IFoobar,?Foobar>().BuildInterceptableServiceProvider().GetRequiredService<Invoker>();invoker.Invoke1();
invoker.Invoke2();
程序執行后,攔截器會以如下的形式將自身的名稱輸出到控制臺上(源代碼)。
三、將攔截器類型定義成特性
其實我們可以讓定義的攔截器類型派生于InterceptorAttribute 特性,這樣就可以直接將它標注到目標類型、屬性和方法上。比如上面這個FoobarInterceptor類型可以改寫成如下的形式。
public?class?FoobarInterceptorAttribute:?InterceptorAttribute
{public?string?Name?{?get;??}public?FoobarInterceptorAttribute(string?name)?=>?Name?=?name;public?ValueTask?InvokeAsync(InvocationContext?invocationContext){Console.WriteLine($"FoobarInterceptor?'{Name}'?is?invoked.");return?invocationContext.ProceedAsync();}
}
那么它就可以按照如下的方式標注到Invoker類型的兩個方法上(源代碼)。
public?class?Invoker
{[FoobarInterceptor("Interceptor1")]public?virtual?void?Invoke1()?=>?Console.WriteLine("Invoker.Invoke1()");[FoobarInterceptor("Interceptor2")]public?virtual?void?Invoke2()?=>?Console.WriteLine("Invoker.Invoke2()");
}
四、合法性檢驗
只有接口方法和虛方法才能被攔截,Dora.Interception針對攔截器的應用提供了如下的驗證邏輯:
標注到方法上(函數屬性的Get/Set方法):如果目標方法均不能被攔截,拋出異常;
標注到屬性上:表示將攔截器應用到該屬性可以被攔截的Get/Set方法上。如果Get和Set方法均不能被攔截,拋出異常;
標注到類型上:表示將攔截器應用到目標類型可以來攔截的方法(含屬性方法)上,如果類型的所有方法均不能被攔截,此時不會拋出異常。
public?class?Foo
{[FoobarInterceptor]public?void?M()?{?}
}public?class?Bar
{[FoobarInterceptor]public?object??P?{?get;?set;?}
}[FoobarInterceptor]
public?class?Baz
{public?void?M()?{?}
}
對于上面定義的三個類型,Foo的M方法和Bar的P屬性均是無法被攔截,Baz類型并沒有可以被攔截的方法。我們采用如下的程序測試上述的檢驗邏輯。
GetService<Foo>();
GetService<Bar>();
GetService<Baz>();static?void?GetService<T>()?where?T:class
{try{Console.WriteLine($"{typeof(T).Name}:");_?=?new?ServiceCollection().AddSingleton<T>().BuildInterceptableServiceProvider().GetRequiredService<T>();Console.WriteLine("OK");}catch?(Exception?ex){Console.WriteLine(ex.Message);}
}
程序運行后會在控制臺上輸出如下的結果,可以看出只有將攔截器應用到不合法的方法和屬性上才會拋出異常(源代碼)。
五、針對類型、屬性的標注
我們利用如下這個攔截器類型FoobarInterceptorAttribute 來演示將攔截器應用到類型和屬性上。該攔截器類型派生于InterceptorAttribute特性,并在執行的時候輸出當前的方法。
public?class?FoobarInterceptorAttribute?:?InterceptorAttribute
{public?ValueTask?InvokeAsync(InvocationContext?invocationContext){var?method?=?invocationContext.MethodInfo;Console.WriteLine($"{method.DeclaringType!.Name}.{method.Name}?is?intercepted.");return?invocationContext.ProceedAsync();}
}
我們將FoobarInterceptorAttribute 特性標注到Foo類型上,后者定義的M1方法和P1屬性是可以被攔截的,但是M2方法和P2屬性則不能。FoobarInterceptorAttribute 特性還被應用到Bar類型的P1屬性以及P2屬性的Set方法上。
[FoobarInterceptor]
public?class?Foo
{public?virtual?void?M1()?{?}public?void?M2()?{?}public?virtual?object??P1?{?get;?set;?}public?object??P2?{?get;???set;?}
}public?class?Bar
{[FoobarInterceptor]public?virtual?object??P1?{?get;?set;?}public?virtual?object??P2?{?get;?[FoobarInterceptor]?set;?}
}
我們利用如下的程序來檢驗針對Foo和Bar對象所有方法和屬性的調用,那么被攔截器攔截下來。
var?provider?=?new?ServiceCollection().AddSingleton<Foo>().AddSingleton<Bar>().BuildInterceptableServiceProvider();var?foo?=?provider.GetRequiredService<Foo>();
var?bar?=?provider.GetRequiredService<Bar>();foo.M1();
foo.M2();
foo.P1?=?null;
_?=?foo.P1;
foo.P2?=?null;
_?=?foo.P2;
Console.WriteLine();bar.P1?=?null;
_?=?bar.P1;
bar.P2?=?null;
_?=?bar.P2;
程序運行之后會在控制臺上輸出如下的結果(源代碼)。
六、攔截的屏蔽
如果某個攔截器需要被應用大某個類型的絕大部分成員,我們可以選擇“排除法”:將攔截器應用到該類型上,將某些非目標成員屏蔽掉。還有一種情況下,如果我們確定某些類型或者方法不能被攔截(比如會在一個循環中頻繁調用),又擔心一些“模糊”的攔截器注冊方法將它們與某些攔截器錯誤地關聯在一起,此時我們可以選擇將其攔截功能顯式屏蔽掉。
針對攔截的屏蔽可以通過在類型、屬性、方法設置程序集上標注NonInterceptableAttribute特性。由于屏蔽功能具有最高優先級,一旦將此特性應用到某個類型上,該類型上的所有成員均不會被攔截。如果被標注到屬性上,其Get和Set方法也不會被攔截。具有如下定義的Foo和Bar類型的所有方法和屬性都不會被攔截(源代碼)。
[FoobarInterceptor]
public?class?Foo
{[NonInterceptable]public?virtual?void?M()?{?}[NonInterceptable]public?virtual?object??P1?{?get;?set;?}public?virtual?object??P2?{?[NonInterceptable]?get;?set;?}
}[NonInterceptable]
public?class?Bar
{[FoobarInterceptor]public?virtual?void?M()?{?}[FoobarInterceptor]public?virtual?object??P?{?get;?set;?}
}