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

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]: 框架設計和實現原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章