多年從事框架設計開發使我有了一種強迫症,那就是見不得一個應用里頻繁地出現重復的代碼。之前經常Review別人的代碼,一看到這樣的程序,我就會想如何將這些重復的代碼寫在一個地方,然后采用“注入”的方式將它們放到需要的程序中。我們知道AOP是解決這類問題最理想的方案。為此,我自己寫了一個AOP框架,該框架被命名為Dora.Interception。Dora.Interception已經在GitHub上開源,如果有興趣的朋友想下載源代碼或者閱讀相關文檔,可以訪問GitHub地址:https://github.com/jiangjinnan/Dora。Demo源代碼下載地址:http://files.cnblogs.com/files/artech/Dora.Interception.Demo.rar
目錄
一、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: }
1: public class Foobar
2: {
3: [ExceptionPolicy("DefaultPolicy")
4: public void Invoke()
5: {
6: ...
7: }
8: }
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: {
7: ...
8: }
9: }
如果我們在類型上應用了某個InterceptorAttribute,但是對應的Interceptor卻並不希望應用到某個方法中,我們可以利用NonInterceptableAttribute采用如下的形式將它們屏蔽,
1: [CacheReturnValue]
2: public class Foobar
3: {
4: ...
5: [NonInterceptable(typeof(CacheReturnValueAttribute)]
6: public Data GetRealTypeData()
7: {...}
8: }
七、以Dependency Injection的形式提供Proxy
我們知道應用在目標類型或者其方法上的Interceptor能夠生效,要求方法調用針對的是封裝目標對象的Proxy對象,換句話說我們希望提供的對象是一個Proxy而不是目標對象。除此之外,我們在上面的設計目標已經提到過,我們希望這個AOP框架能夠與.NET Core的Dependency Injection框架進行無縫集成,所以現在的問題變成了:如何讓Dependency Injection的ServiceProvider提供的是Proxy對象,而不是目標對象。我提供的兩種方案來解決這個問題,接下來我們通過一個ASP.NET Core MVC應用來舉例說明。
為了能夠使用上面提供的CacheInterceptor並且能夠以很直觀的方式感受到緩存的存在,我定義了如下這個表示系統時鍾的ISystemClock接口和具體實現類型SystemClock。從如下的代碼片段可以看出,GetCurrentTime方法總是返回實時的時間,但是由於應用了CaheReturnValueAttribute,如果CacheInterceptor生效,返回的時間在緩存過期之前總是相同的。
1: public interface ISystomClock
2: {
3: DateTime GetCurrentTime();
4: }
5:
6: public class SystomClock : ISystomClock
7: {
8: [CacheReturnValue]
9: public DateTime GetCurrentTime()
10: {
11: return DateTime.UtcNow;
12: }
13: }
我們在HomeController中以構造器注入的方式來使用ISystemClock。在默認情況下,如果我們注入的類型ISystemClock接口,那么毫無疑問,那么GetCurrentTime方法調用的就是SystemClock對象本身,所以根本不可能起到緩存的作用。所以我們將注入類型替換成IInterceptable<ISystomClock>,后者的Proxy屬性將會返回我們希望的Proxy對象。
1: public class HomeController : Controller
2: {
3: private readonly ISystomClock _clock;
4: public HomeController(IInterceptable<ISystomClock> clockAccessor)
5: {
6: _clock = clockAccessor.Proxy;
7: }
8:
9: [HttpGet("/")]
10: public async Task Index()
11: {
12: this.Response.ContentType = "text/html";
13: await this.Response.WriteAsync("<html><body><ul>");
14: for (int i = 0; i < 5; i++)
15: {
16: await this.Response.WriteAsync($"<li>{_clock.GetCurrentTime()}({DateTime.UtcNow})</li>");
17: await Task.Delay(1000);
18: }
19: await this.Response.WriteAsync("</ul><body></html>");
20: }
21: }
當然我們需要注冊Dora.Interception一些必須的服務,這些服務采用如下的形式通過調用擴展方法AddInterception來實現。
1: public class Startup
2: {
3: public void ConfigureServices(IServiceCollection services)
4: {
5: services
6: .AddScoped<ISystomClock, SystomClock>()
7: .AddInterception(builder=>builder.SetDynamicProxyFactory())
8: .AddMvc();
9: }
10: public void Configure(IApplicationBuilder app)
11: {
12: app.UseMvc();
13: }
14: }
雖然IInterceptable<T>能夠解決Proxy的提供問題,但是這種編程模式其實是很不好的。理想的編程模式應該是:依賴某個服務就注入對應的服務接口就可以。這個問題其實也好解決,我們首先將HomeController還原成典型的編程模式:
1: public class HomeController : Controller
2: {
3: private readonly ISystomClock _clock;
4: public HomeController(ISystomClock clock)
5: {
6: _clock = clock;
7: }
8:
9: [HttpGet("/")]
10: public async Task Index()
11: {
12: this.Response.ContentType = "text/html";
13: await this.Response.WriteAsync("<html><body><ul>");
14: for (int i = 0; i < 5; i++)
15: {
16: await this.Response.WriteAsync($"<li>{_clock.GetCurrentTime()}({DateTime.UtcNow})</li>");
17: await Task.Delay(1000);
18: }
19: await this.Response.WriteAsync("</ul><body></html>");
20: }
21: }
接下來我們只需要修改Startup的ConfigureServices的兩個地方同樣達到相同的目的。如下面的代碼片段所示,我們讓ConfigureServices返回一個IServiceProvider對象,這個對象直接調用我們定義的擴展方法BuilderInterceptableServiceProvider來創建。
1: public class Startup
2: {
3: public IServiceProvider ConfigureServices(IServiceCollection services)
4: {
5: services
6: .AddScoped<ISystomClock, SystomClock>()
7: .AddMvc();
8: return services.BuilderInterceptableServiceProvider(builder => builder.SetDynamicProxyFactory());
9: }
10:
11: public void Configure(IApplicationBuilder app)
12: {
13: app.UseMvc();
14: }
15: }
對於上述的兩種編程模式,運行程序后瀏覽器上都會呈現出相同的時間:

