多年從事框架設計開發使我有了一種強迫症,那就是見不得一個應用里頻繁地出現重覆的代碼。之前經常Review別人的代碼,一看到這樣的程式,我就會想如何將這些重覆的代碼寫在一個地方,然後採用“註入”的方式將它們放到需要的程式中。我們知道AOP是解決這類問題最理想的方案。為此,我自己寫了一個AOP框架,該框 ...
多年從事框架設計開發使我有了一種強迫症,那就是見不得一個應用里頻繁地出現重覆的代碼。之前經常Review別人的代碼,一看到這樣的程式,我就會想如何將這些重覆的代碼寫在一個地方,然後採用“註入”的方式將它們放到需要的程式中。我們知道AOP是解決這類問題最理想的方案。為此,我自己寫了一個AOP框架,該框架被命名為Dora.Interception。Dora.Interception已經在GitHub上開源,如果有興趣的朋友想下載源代碼或者閱讀相關文檔,可以訪問GitHub地址:https://github.com/jiangjinnan/Dora。
目錄
一、Dora, 為什麼叫這個名字?
二、Dora.Interception的設計目標
三、以怎樣的方式使用Dora.Interception
四、如何定義一個Interceptor
五、定義InterceptorAttribute
六、應用InterceptorAttribute
七、以Dependency Injection的形式提供Proxy
一、Dora, 為什麼叫這個名字?
其實我最早的想法是創建一個IoC框架,並將它命名為Doraemon(哆啦A夢),因為我覺得一個理想的IoC Container就像是機器貓的二次元口袋一樣能夠提供給你期望的一切服務對象。後來覺得這名字太長,所以改名為Dora。雖然Dora這個名字聽上去有點“娘”,並且失去了原本的意思,但是我很喜歡這個單詞的一種釋義——“上帝的禮物”之一。在接觸了.NET Core的時候,我最先研究的就是它基於ServiceCollection和ServiceProvider的Dependency Injection框架,雖然這個框架比較輕量級,但是能夠滿足絕大部分項目的需求,所以我放棄了初衷。不過我依然保留了Dora這個開源項目名,併為此購買了一個功能變數名稱(doranet.org),我希望將我多年的一些想法以一系列開源框架的形式實現出來,Dora.Interception就是Dora項目的第一個基於AOP的框架。
二、Dora.Interception的設計目標
我當初在設計Dora.Interception框架時給自己確定的幾個目標:
- Dora.Interception一個基於運行時(Run Time),而不是針對編譯時(Compile Time)的AOP框架。它通過在運行時動態創建代理對象(Proxy)來封裝目標對象(Target),並自動註入應用的攔截器(Interceptor),而不是在編譯時幫助你生成一個Proxy類型。
- Dora.Interception需要採用一種優雅的方式來定義和應用Interceptor。
- 能夠與.NET Core的Dependency Injection框架無縫集成
- 能夠整合其他AOP框架。實際上Dora.Interception並沒有自行實現最底層的“攔截”機制,我使用的是Castle的DynamicProxy。如果有其他的選擇,我們可以很容易地將它引入進來。
三、以怎樣的方式使用Dora.Interception
Dora.Interception目前的版本為1.1.0,由如下兩個NuGet包來承載,由於Dora.Interception.Castle依賴於Dora.Interception,所以安裝後者即可。
- Dora.Interception: 提供基本的API
- Dora.Interception.Castle: 提供基於Castle(DynamicProxy)的攔截實現
四、如何定義一個Interceptor
接下來我們通過一個簡單的實例來說明一下如何採用“優雅”的方式來定義一個Interceptor類型。我們即將定義的這個CacheInterceptor可以應用到某個具有返回值的方法上實現針對返回值的緩存。如果應用了這個Interceptor,它根據傳入的參數對返回的值實施緩存。如果後續調用傳入了相同的參數,並且之前的緩存尚未過期,緩存的結果將直接作為方法的返回值,從而避免了針對目標方法的重覆調用。針對的緩存功能實現在如下這個CacheInterceptor類型中,可以看出針對的緩存是利用MemoryCache來完成的。
1: public class CacheInterceptor
2: {
3: private readonly InterceptDelegate _next;
4: private readonly IMemoryCache _cache;
5: private readonly MemoryCacheEntryOptions _options;
6:
7: public CacheInterceptor(InterceptDelegate next, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
8: {
9: _next = next;
10: _cache = cache;
11: _options = optionsAccessor.Value;
12: }
13:
14: public async Task InvokeAsync(InvocationContext context)
15: {
21: var key = new Cachekey(context.Method, context.Arguments);
22: if (_cache.TryGetValue(key, out object value))
23: {
24: context.ReturnValue = value;
25: }
26: else
27: {
28: await _next(context);
29: _cache.Set(key, context.ReturnValue, _options);
30: }
31: }
32: public class CacheKey {...}
33: }
CacheInterceptor體現了一個典型的Interceptor的定義方式:
- Interceptor類型無需實現任何的介面,我們只需要定義一個普通的公共實例類型即可。
- Interceptor類型必須具有一個公共構造函數,並且該構造函數的第一個參數的類型必須是InterceptDelegate,後者代表的委托對象會幫助我們調用後一個Interceptor或者目標方法(如果當前Interceptor已經是最後一個了)。
- 上述這個構造函數可以包含任意的參數(比如CacheInterceptor構造函數中的cache和optionsAccessor)。這些參數可以直接利用.NET Core的Dependency Injection的方式進行註冊,對於沒有註冊的參數需要在應用該Interceptor的時候顯式提供。
- 攔截功能實現在約定的InvokeAsync的方法中,這是一個返回類型為Task的非同步方法,它的第一個參數類型為InvocationContext,代表當前方法調用的上下文。我們可以利用這個上下文對象得到Proxy對象和目標對象,代表當前調用方法的MethodInfo對象,以及傳入的輸入參數等。除此之外,我們也可以利用這個上下文直接設置方法的返回值或者輸出參數。
- 這個InvokeAsync方法可以包含任意後續參數,但是要求這些參數預先以Dependency Injection的形式進行註冊。這也是我沒有定義一個介面來表示Interceptor的原因,因為這樣就不能將依賴的服務直接註入到InvokeAsync方法中了。
- 當前Interceptor是否調用後續的Interceptor或者目標方法,取決於你是否調用構造函數傳入的這個InterceptDelegate委托對象。
由於依賴的服務對象(比如CacheInterceptor依賴IMemoryCache 和IOptions<MemoryCacheEntryOptions>對象)可以直接註入到InvokeAsync方法中,所以上述這個CacheInterceptor也可以定義成如下的形式
1: public class CacheInterceptor
2: {
3: private readonly InterceptDelegate _next;
4: public CacheInterceptor(InterceptDelegate next)
5: {
6: _next = next;
7: }
8:
9: public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor)
10: {
11: if (!context.Method.GetParameters().All(it => it.IsIn))
12: {
13: await _next(context);
14: }
15:
16: var key = new Cachekey(context.Method, context.Arguments);
17: if (cache.TryGetValue(key, out object value))
18: {
19: context.ReturnValue = value;
20: }
21: else
22: {
23: await _next(context);
24: _cache.Set(key, context.ReturnValue, optionsAccessor.Value);
25: }
26: }
27: }
五、定義InterceptorAttribute
我們採用Attribute的形式來將對應的Intercepor應用到某個類型或者方法上,每個具體的Interceptor類型都具有對應的Attribute。這樣的Attribute直接繼承基類InterceptorAttribute。如下這個CacheReturnValueAttribute就是上面這個CacheInterceptor對應的InterceptorAttribute。
1: [AttributeUsage(AttributeTargets.Method)]
2: public class CacheReturnValueAttribute : InterceptorAttribute
3: {
4: public override void Use(IInterceptorChainBuilder builder)
5: {
6: builder.Use<CacheInterceptor>(this.Order);
7: }
8: }
具體的InterceptorAttribute只需要重寫Use方法將對應的Interceptor添加到Interceptor管道之中,這個功能可以直接調用作為參數的InterceptorChainBuilder對象的泛型方法Use<TInterceptor>來實現。對於這個泛型方法來說,泛型參數類型代表目標Interceptor的類型,而第一個參數表示註冊的Interceptor在整個管道中的位置。如果創建目標Interceptor而調用的構造函數的參數尚未採用Dependency Injection的形式註冊,我們需要在這個方法中提供。對於CacheInterceptor依賴的兩個對象(IMemoryCache 和IOptions<MemoryCacheEntryOptions>)都可以採用Dependency Injection的形式註入,所以我們在調用Use<CacheInterceptor>方法是並不需要提供這個兩個參數。
假設我們定義一個ExceptionHandlingInterceptor來實施自動化異常處理,當我們在創建這個Interceptor的時候需要提供註冊的異常處理類型的名稱,那麼我們需要採用如下的形式來定義對應的這個IntercecptorAttribute。如下麵的代碼片段所示,我們在調用Use<ExceptionHandlingInterceptor>方法的時候就需要顯式指定這個策略名稱。
1: [AttributeUsage(AttributeTargets.Method|AttributeTargets.)]
2: public class HandleExceptionAttribute : InterceptorAttribute
3: {
4: public string ExceptionPolicy {get;}
5: public string HandleExceptionAttribute(string exceptionPolicy)
6: {
7: this.ExceptionPolicy = exceptionPolicy;
8: }
9: public override void Use(IInterceptorChainBuilder builder)
10: {
11: builder.Use<ExceptionHandlingInterceptor>(this.Order,this.ExceptionPolicy);
12: }
13: }
有的時候,IntercecptorAttribute在註冊對應Interceptor的時候需要使用到應用到當前方法或者類型上的其他Attribute。舉個簡單的例子,上述的這個HandleExceptionAttribute實際上是自動提供異常處理策略名稱,假設異常處理系統自身使用另外一個獨立的ExceptionPolicyAttribute採用如下的形式來提供這個策略。1: public class Foobar
2: {
3: [ExceptionPolicy("DefaultPolicy")
4: public void Invoke()
5: {
6: ...
7: }
8: }
這個問題很好解決,因為InterceptorAttribute自身提供了應用到目標方法或者類型上的所有Attribute,所以上述這個HandleExceptionAttribute可以採用如下的定義方式。 1: [AttributeUsage(AttributeTargets.Method)]
2: public class HandleExceptionAttribute : InterceptorAttribute
3: {
4: public override void Use(IInterceptorChainBuilder builder)
5: {
6: ExceptionPolicyAttribute attribute = this.Attributes.ofType<ExceptionPolicyAttribute>().First();
7: builder.Use<Exception>(this.Order, attribute.ExceptionPolicy);
8: }
9: }
六、應用InterceptorAttribute
Interceptor通過對應的InterceptorAttribute被應用到某個方法或者類型上,我們在應用InterceptorAttribute可以利用其Order屬性確定Interceptor的排列(執行)順序。如下麵的代碼片段所示, HandleExceptionAttribute和CacheReturnValueAttribute分別被應用到Foobar類型和Invoke方法上,我要求ExceptionHandlingInterceptor能夠處理CacheInterceptor拋出的異常, 那麼前者必須由於後者執行,所以我通過Order屬性控制了它們的執行順序。值得一提的是,目前我們支持兩個攔截機制,一種是基於介面,另一種是基於虛方法。如果採用基於介面的攔截機制,我要求InterceptorAttribute應用在實現類型或者其方法上,應用在介面和其方法上的InterceptorAttribute將無效。
1: [HandleException("defaultPolicy", Order = 1)]
2: public class Foobar: IFoobar
3: {
4: [CacheReturnValue(this.Order = 2)]
5: public Data LoadData()
6: {