Dora.Interception,為.NET Core度身打造的AOP框架 [1]:更加簡練的程式設計體驗
很久之前開發了一個名為Dora.Interception的開源AOP框架(github地址: ofollow,noindex" target="_blank">https://github.com/jiangjinnan/Dora ,如果你覺得這個這框架還有那麼一點價值,請不吝多點一顆星),最近對它作了一些改進(包括程式設計模式和效能,目前版本升級到2.1.2)。一直以來我對軟體設計秉承的一個理念就是:好的設計應該是 簡單 的設計。和其他AOP框架相比,雖然Dora.Interception提供的程式設計模式已經顯得足夠簡單,但是我覺得還應該再簡單點,再簡單點。這個新版本對攔截器的定義和應用提供了更加簡單的定義方式,同時對擴充套件性方法作了較大的改進,接下來我們通過一個簡單例項來體驗一下。原始碼從這裡下載。
一、定義攔截器型別
Dora.Interception中的攔截器型別不需要實現任何的介面或者繼承任何的基類,因為我們採用“ 基於約定 ”的設計方案。由於Dora.Interception是建立在.NET Core的依賴注入框架之上,所以我們可以將任意依賴的服務直接注入到定義的截器型別中。接下來我們將定義一個名為CacheInterceptor的攔截器來實現針對方法返回值的快取。由於快取的內容是某個方法的返回值,所以我們將方法和引數列表作為快取的Key,這個Key由如下這個CacheKey來表示(完整定義請參閱原始碼)。
public class CacheKey { public MethodBase Method { get; } public object[] InputArguments { get; } public CacheKey(MethodBase method, object[] arguments) { this.Method = method; this.InputArguments = arguments; } public override bool Equals(object obj); public override int GetHashCode();}
我們直接利用ASP.NET Core基於記憶體的 快取框架 來對方法返回值實施快取,所以我們直接將IMemoryCache服務和對應的Options以如下的方式 注入 到CacheInterceptor的建構函式中。具體的攔截操作實現在按照約定定義的InvokeAsync方法中,我們可以利用作為輸入引數的InvocationContext 物件得到當前方法呼叫的所有上下文資訊,也可以直接通過它的ReturnValue設定方法的返回值。在如下所示的程式碼片段中,我們正是利用這個InvocationContext物件得到表示當前呼叫方法的MethodInfo物件和輸入引數,並以它們創建出CacheKey物件來操作快取。
public class CacheInterceptor { private readonly IMemoryCache _cache; private readonly MemoryCacheEntryOptions _options; public CacheInterceptor(IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor) { _cache = cache; _options = optionsAccessor.Value; } public async Task InvokeAsync(InvocationContext context) { var key = new CacheKey(context.Method, context.Arguments); if (_cache.TryGetValue(key, out object value)) { context.ReturnValue = value; } else { await context.ProceedAsync(); _cache.Set(key, context.ReturnValue, _options); } } }
對於一個攔截器物件來說,當呼叫被其攔截之後,需要由它自己來決定是否需要繼續後續的呼叫,在新的版本中,我們採用直接呼叫InvocationContext的 ProceedAsync 方法的方式來達到這個目的。上面這個CacheInterceptor型別採用構造器注入的方式來注入依賴的服務,實際上我們還具有更加簡單的方案,那就是採用如下的方式直接將依賴服務注入到InvokeAsync方法中。
public class CacheInterceptor { public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor) { var key = new CacheKey(context.Method, context.Arguments); if (cache.TryGetValue(key, out object value)) { context.ReturnValue = value; } else { await context.ProceedAsync(); cache.Set(key, context.ReturnValue, optionsAccessor.Value); } } }
二、應用攔截器
所謂的Interceptor應用就是如何將Interceptor應用到定義在某個型別上的某個方法的過程。Dora.Interception預設提供了多種註冊方式,最為常用的莫過於採用Attribute標註的方式來註冊Interceptor。如果需要採用這種方式來註冊CacheInterceptor,我們需要採用如下的方式為Interceptor型別定義對應的CacheReturnValueAttribute 型別。CacheReturnValueAttribute 派生於抽象類InterceptorAttribute,在重寫的Use方法中,它呼叫作為引數的IInterceptorChainBuilder 物件的Use方法將CacheInterceptor新增到Interceptor管道中,傳入的引數(Order)代表Interceptor在管道中的位置。
[AttributeUsage(AttributeTargets.Method)] public class CacheReturnValueAttribute : InterceptorAttribute { public override void Use(IInterceptorChainBuilder builder) => builder.Use<CacheInterceptor>(Order); }
Dora.Interception刻意地將Interceptor和對應的Attribute區分物件,因為我們認為後者僅僅是Interceptor的一種“註冊方式'’而已。如果我們希望將二者合一,我們可以採用如下的定義方式。
public class CacheInterceptorAttribute : InterceptorAttribute { public async Task InvokeAsync(InvocationContext context, IMemoryCache cache, IOptions<MemoryCacheEntryOptions> optionsAccessor) { var key = new CacheKey(context.Method, context.Arguments); if (cache.TryGetValue(key, out object value)) { context.ReturnValue = value; } else { await context.ProceedAsync(); cache.Set(key, context.ReturnValue, optionsAccessor.Value); } } public override void Use(IInterceptorChainBuilder builder) => builder.Use(this, Order); }
為了演示CacheInterceptor針對目標返回值的快取,我們定義瞭如下這個標識“系統時鐘”的ISystemClock服務,它的GetCurrentTime方法返回當前的時間。為了驗證基於引數的快取,我們為該方法定義了一個表示事件型別(Local或者UTC)的引數。上面定義的CacheReturnValueAttribute標註在實現型別的GetCurrentTime方法上。很多AOP框架都支援將Interceptor直接應用到服務介面上,但我個人覺得這是不對的,因為介面表示的是雙邊契約,Interceptor體現的是 單邊的行為 ,所以Interceptor是不應該應用到介面上。
public interface ISystemClock { DateTime GetCurrentTime(DateTimeKind dateTimeKind); } public class DefaultSystemClock : ISystemClock { [CacheReturnValue] public DateTime GetCurrentTime(DateTimeKind dateTimeKind) => dateTimeKind == DateTimeKind.Utc ? DateTime.UtcNow : DateTime.Now; }
三、你的程式碼不需要任何改變
將Dora.Interception引入你的應用完全不會影響你現有的程式碼,比如在消費ISystemClock服務的時候完全不用考慮CacheInterceptor的存在。如下所示的就是典型地在Controller中以注入形式消費服務的程式設計模式。
public class HomeController: Controller { private readonly ISystemClock _clock; public HomeController(ISystemClock clock)=> _clock = clock; [HttpGet("/")] public async Task Index() { async Task<string[]> GetTimesAsync() { var times = new string[6]; for (int index = 0; index < 3; index++) { times[index] = $"Local: {_clock.GetCurrentTime(DateTimeKind.Local)}"; await Task.Delay(1000); } for (int index = 3; index < 6; index++) { times[index] = $"UTC: {_clock.GetCurrentTime(DateTimeKind.Utc)}"; await Task.Delay(1000); } return times; } var currentTimes = await GetTimesAsync(); var list = string.Join("", currentTimes.Select(it => $"<li>{it}</li>")); Response.ContentType = "text/html"; await Response.WriteAsync( @"<html> <body> <ul>" + list + @"</ul> </body> </html>");} }
我們唯一需要做的就是在註冊Startup型別的ConfigureServices方法中呼叫IServiceCollection的擴充套件方法 BuildInterceptableServiceProvider 方法建立並返回一個IServiceProvider,後者能夠幫助我們創建出能夠被攔截的服務例項。BuildInterceptableServiceProvider方法提供的是一個InterceptableServiceProvider物件,InterceptableServiceProvider與目前.NET Core DI框架基本上是一致的,我僅僅對它作了一些微小的改動。
public class Startup { public IServiceProvider ConfigureServices(IServiceCollection services) { services .AddSingleton<ISystemClock, DefaultSystemClock>() .AddMemoryCache() .AddMvc(); return services.BuildInterceptableServiceProvider(); } public void Configure(IApplicationBuilder app) => app.UseDeveloperExceptionPage().UseMvc(); }
如果執行上面這個簡單的ASP.NET Core MVC應用,瀏覽器將會呈現出如下所示的輸出結果。由於SystemClock的GetCurrentTime方法的返回值被快取了,所以針對相同引數返回的時間是相同的。