Dora.Interception,爲.NET Core度身打造的AOP框架 [1]:更加簡練的編程體驗

很久之前開發了一個名爲Dora.Interception的開源AOP框架(github地址: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方法的返回值被緩存了,所以針對相同參數返回的時間是相同的。

[1]:更加簡練的編程體驗 [2]:基於約定的攔截器定義方式 [3]:多樣性的攔截器應用方式 [4]:與依賴注入框架的深度整合 [5]:對攔截機制的靈活定製

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