全新升級的AOP框架Dora.Interception[3]: 基於特性標註的攔截器註冊方式

2022-06-22 12:02:18

Dora.Interception(github地址,覺得不錯不妨給一顆星)中按照約定方式定義的攔截器可以採用多種方式註冊到目標方法上。本篇文章介紹最常用的基於「特性標註」的攔截器註冊方式,下一篇會介紹另一種基於(Lambda)表示式的註冊方式。如果原生定義的這兩種註冊方式不能滿足要求,利用框架提供的擴充套件,我們可以完成任何你想要的攔截器註冊手段。(拙著《ASP.NET Core 6框架揭祕》於日前上市,加入讀者群享6折優惠)

目錄
一、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();

程式執行後,攔截器會以如下的形式將自身的名稱輸出到控制檯上(原始碼)。

image

三、將攔截器型別定義成特性

其實我們可以讓定義的攔截器型別派生於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);
    }
}

程式執行後會在控制檯上輸出如下的結果,可以看出只有將攔截器應用到不合法的方法和屬性上才會丟擲異常(原始碼)。

image

五、針對型別、屬性的標註

我們利用如下這個攔截器型別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;

程式執行之後會在控制檯上輸出如下的結果(原始碼)。

image

六、攔截的遮蔽

如果某個攔截器需要被應用大某個型別的絕大部分成員,我們可以選擇「排除法」:將攔截器應用到該型別上,將某些非目標成員遮蔽掉。還有一種情況下,如果我們確定某些型別或者方法不能被攔截(比如會在一個迴圈中頻繁呼叫),又擔心一些「模糊」的攔截器註冊方法將它們與某些攔截器錯誤地關聯在一起,此時我們可以選擇將其攔截功能顯式遮蔽掉。

針對攔截的遮蔽可以通過在型別、屬性、方法設定程式集上標註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; }
}

全新升級的AOP框架Dora.Interception[1]: 程式設計體驗
全新升級的AOP框架Dora.Interception[2]: 基於約定的攔截器定義方式
全新升級的AOP框架Dora.Interception[3]: 基於「特性標註」的攔截器註冊方式
全新升級的AOP框架Dora.Interception[4]: 基於「Lambda表示式」的攔截器註冊方式
全新升級的AOP框架Dora.Interception[5]: 實現任意的攔截器註冊方式
全新升級的AOP框架Dora.Interception[6]: 框架設計和實現原理