Dora.Interception: 一个为.NET Core度身定制的AOP框架
多年從事框架設計開發使我有了一種強迫癥,那就是見不得一個應用里頻繁地出現重復的代碼。之前經常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:???? { 16:???????? if (!context.Method.GetParameters().All(it => it.IsIn)) 17:???????? { 18:???????????? await _next(context); 19:???????? } 20:? 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 {...}
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:???? { 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: }對于上述的兩種編程模式,運行程序后瀏覽器上都會呈現出相同的時間:
總結
以上是生活随笔為你收集整理的Dora.Interception: 一个为.NET Core度身定制的AOP框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle sql语句怎么查询所有存储
- 下一篇: 配资怎么开户