在ASP.NET Core中使用AOP來簡化緩存操做

前言

關於緩存的使用,相信你們都是熟悉的不能再熟悉了,簡單來講就是下面一句話。git

優先從緩存中取數據,緩存中取不到再去數據庫中取,取到了在扔進緩存中去。github

而後咱們就會看到項目中有相似這樣的代碼了。數據庫

public Product Get(int productId)
{
    var product = _cache.Get($"Product_{productId}");
    
    if(product == null)
    {
        product = Query(productId);
        
        _cache.Set($"Product_{productId}",product ,10);
    }

    return product;
}

然而在初期,沒有緩存的時候,可能這個方法就一行代碼。緩存

public Product Get(int productId)
{
    return Query(productId);
}

隨着業務的不斷髮展,可能會出現愈來愈多相似第一段的示例代碼。這樣就會出現大量「重複的代碼」了!框架

顯然,咱們不想讓這樣的代碼處處都是!async

基於這樣的情景下,咱們徹底可使用AOP去簡化緩存這一部分的代碼。ide

大體的思路以下 :函數

在某個有返回值的方法執行前去判斷緩存中有沒有數據,有就直接返回了;單元測試

若是緩存中沒有的話,就是去執行這個方法,拿到返回值,執行完成以後,把對應的數據寫到緩存中去,測試

下面就根據這個思路來實現。

本文分別使用了Castle和AspectCore來進行演示。

這裏主要是作了作了兩件事

  1. 自動處理緩存的key,避免硬編碼帶來的坑
  2. 經過Attribute來簡化緩存操做

下面就先從Castle開始吧!

使用Castle來實現

通常狀況下,我都會配合Autofac來實現,因此這裏也不例外。

咱們先新建一個ASP.NET Core 2.0的項目,經過Nuget添加下面幾個包(固然也能夠直接編輯csproj來完成的)。

<PackageReference Include="Autofac" Version="4.6.2" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.2.1" />
<PackageReference Include="Castle.Core" Version="4.2.1" />

而後作一下前期準備工做

1.緩存的使用

定義一個ICachingProvider和其對應的實現類MemoryCachingProvider

簡化了一下定義,就留下讀和取的操做。

public interface ICachingProvider
{
    object Get(string cacheKey);

    void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow);
}

public class MemoryCachingProvider : ICachingProvider
{
    private IMemoryCache _cache;

    public MemoryCachingProvider(IMemoryCache cache)
    {
        _cache = cache;
    }

    public object Get(string cacheKey)
    {
        return _cache.Get(cacheKey);
    }

    public void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow)
    {
        _cache.Set(cacheKey, cacheValue, absoluteExpirationRelativeToNow);
    }
}

2.定義一個Attribute

這個Attribute就是咱們使用時候的關鍵了,把它添加到要緩存數據的方法中,便可完成緩存的操做。

這裏只用了一個絕對過時時間(單位是秒)來做爲演示。若是有其餘緩存的配置,也是能夠往這裏加的。

[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class QCachingAttribute : Attribute
{
    public int AbsoluteExpiration { get; set; } = 30;

    //add other settings ...
}

3.定義一個空接口

這個空接口只是爲了作一個標識的做用,爲了後面註冊類型而專門定義的。

public interface IQCaching
{
}

4.定義一個與緩存鍵相關的接口

定義這個接口是針對在方法中使用了自定義類的時候,識別出這個類對應的緩存鍵。

public interface IQCachable
{
    string CacheKey { get; }
}

準備工做就這4步(AspectCore中也是要用到的),

下面咱們就是要去作方法的攔截了(攔截器)。

攔截器首先要繼承並實現IInterceptor這個接口。

public class QCachingInterceptor : IInterceptor
{
    private ICachingProvider _cacheProvider;

    public QCachingInterceptor(ICachingProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }

    public void Intercept(IInvocation invocation)
    {
        var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);
        if (qCachingAttribute != null)
        {
            ProceedCaching(invocation, qCachingAttribute);
        }
        else
        {
            invocation.Proceed();
        }
    }
}

有兩點要注意:

  1. 由於要使用緩存,因此這裏須要咱們前面定義的緩存操做接口,而且在構造函數中進行注入。
  2. Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的惟必定義。

Intercept方法其實很簡單,獲取一下當前執行方法是否是有咱們前面自定義的QCachingAttribute,有的話就去處理緩存,沒有的話就是僅執行這個方法而已。

下面揭開ProceedCaching方法的面紗。

private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute)
{
    var cacheKey = GenerateCacheKey(invocation);

    var cacheValue = _cacheProvider.Get(cacheKey);
    if (cacheValue != null)
    {
        invocation.ReturnValue = cacheValue;
        return;
    }

    invocation.Proceed();

    if (!string.IsNullOrWhiteSpace(cacheKey))
    {
        _cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));
    }
}

這個方法,就是和大部分操做緩存的代碼同樣的寫法了!

注意下面幾個地方

  1. invocation.Proceed()表示執行當前的方法
  2. invocation.ReturnValue是要執行後纔會有值的。
  3. 在每次執行前,都會依據當前執行的方法去生成一個緩存的鍵。

下面來看看生成緩存鍵的操做。

這裏生成的依據是當前執行方法的名稱,參數以及該方法所在的類名。

生成的代碼以下:

private string GenerateCacheKey(IInvocation invocation)
{
    var typeName = invocation.TargetType.Name;
    var methodName = invocation.Method.Name;
    var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);

    return this.GenerateCacheKey(typeName, methodName, methodArguments);
}
//拼接緩存的鍵
private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters)
{
    var builder = new StringBuilder();

    builder.Append(typeName);
    builder.Append(_linkChar);

    builder.Append(methodName);
    builder.Append(_linkChar);

    foreach (var param in parameters)
    {
        builder.Append(param);
        builder.Append(_linkChar);
    }

    return builder.ToString().TrimEnd(_linkChar);
}

private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5)
{
    return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList();
}
//處理方法的參數,可根據狀況自行調整
private string GetArgumentValue(object arg)
{
    if (arg is int || arg is long || arg is string)
        return arg.ToString();

    if (arg is DateTime)
        return ((DateTime)arg).ToString("yyyyMMddHHmmss");

    if (arg is IQCachable)
        return ((IQCachable)arg).CacheKey;

    return null;
}

這裏要注意的是GetArgumentValue這個方法,由於一個方法的參數有多是基本的數據類型,也有多是本身定義的類。

對於本身定義的類,必需要去實現IQCachable這個接口,而且要定義好鍵要取的值!

若是說,在一個方法的參數中,有一個自定義的類,可是這個類卻沒有實現IQCachable這個接口,那麼生成的緩存鍵將不會包含這個參數的信息。

舉個生成的例子:

MyClass:MyMethod:100:abc:999

到這裏,咱們緩存的攔截器就已經完成了。

下面是刪除了註釋的代碼(可去github上查看完整的代碼)

public class QCachingInterceptor : IInterceptor
{
    private ICachingProvider _cacheProvider;
    private char _linkChar = ':';

    public QCachingInterceptor(ICachingProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }

    public void Intercept(IInvocation invocation)
    {
        var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);
        if (qCachingAttribute != null)
        {
            ProceedCaching(invocation, qCachingAttribute);
        }
        else
        {
            invocation.Proceed();
        }
    }

    private QCachingAttribute GetQCachingAttributeInfo(MethodInfo method)
    {
        return method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(QCachingAttribute)) as QCachingAttribute;
    }

    private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute)
    {
        var cacheKey = GenerateCacheKey(invocation);

        var cacheValue = _cacheProvider.Get(cacheKey);
        if (cacheValue != null)
        {
            invocation.ReturnValue = cacheValue;
            return;
        }

        invocation.Proceed();

        if (!string.IsNullOrWhiteSpace(cacheKey))
        {
            _cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));
        }
    }

    private string GenerateCacheKey(IInvocation invocation)
    {
        var typeName = invocation.TargetType.Name;
        var methodName = invocation.Method.Name;
        var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);

        return this.GenerateCacheKey(typeName, methodName, methodArguments);
    }

    private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters)
    {
        var builder = new StringBuilder();

        builder.Append(typeName);
        builder.Append(_linkChar);

        builder.Append(methodName);
        builder.Append(_linkChar);

        foreach (var param in parameters)
        {
            builder.Append(param);
            builder.Append(_linkChar);
        }

        return builder.ToString().TrimEnd(_linkChar);
    }

    private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5)
    {
        return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList();
    }

    private string GetArgumentValue(object arg)
    {
        if (arg is int || arg is long || arg is string)
            return arg.ToString();

        if (arg is DateTime)
            return ((DateTime)arg).ToString("yyyyMMddHHmmss");

        if (arg is IQCachable)
            return ((IQCachable)arg).CacheKey;

        return null;
    }
}

下面就是怎麼用的問題了。

這裏考慮了兩種用法:

  • 一種是面向接口的用法,也是目前比較流行的用法
  • 一種是傳統的,相似經過實例化一個BLL層對象的方法。

先來看看面向接口的用法

public interface IDateTimeService
{        
    string GetCurrentUtcTime();
}

public class DateTimeService : IDateTimeService, QCaching.IQCaching
{
    [QCaching.QCaching(AbsoluteExpiration = 10)]
    public string GetCurrentUtcTime()
    {
        return System.DateTime.UtcNow.ToString();
    }
}

簡單起見,就返回當前時間了,也是看緩存是否生效最簡單有效的辦法。

在控制器中,咱們只須要經過構造函數的方式去注入咱們上面定義的Service就能夠了。

public class HomeController : Controller
{
    private IDateTimeService _dateTimeService;

    public HomeController(IDateTimeService dateTimeService)
    {
        _dateTimeService = dateTimeService;
    }

    public IActionResult Index()
    {
        return Content(_dateTimeService.GetCurrentUtcTime());
    }
}

若是這個時候運行,確定是會出錯的,由於咱們尚未配置!

去Starpup中修改一下ConfigureServices方法,完成咱們的注入和啓用攔截操做。

public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddScoped<ICachingProvider, MemoryCachingProvider>();

        return this.GetAutofacServiceProvider(services);
    }

    private IServiceProvider GetAutofacServiceProvider(IServiceCollection services)
    {
        var builder = new ContainerBuilder();
        builder.Populate(services);
        var assembly = this.GetType().GetTypeInfo().Assembly;
        builder.RegisterType<QCachingInterceptor>();
        //scenario 1
        builder.RegisterAssemblyTypes(assembly)
                     .Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract)
                     .AsImplementedInterfaces()
                     .InstancePerLifetimeScope()
                     .EnableInterfaceInterceptors()
                     .InterceptedBy(typeof(QCachingInterceptor));
       
        return new AutofacServiceProvider(builder.Build());
    }
    
    //other ...
}

要注意的是這個方法原來是沒有返回值的,如今須要調整爲返回IServiceProvider

這段代碼,網上其實有不少解釋,這裏就再也不細說了,主要是EnableInterfaceInterceptorsInterceptedBy

下面是運行的效果:

再來看看經過實例化的方法

先定義一個BLL層的方法,一樣是返回當前時間。這裏咱們直接把Attribute放到這個方法中便可,同時還要注意是virtual的。

public class DateTimeBLL : QCaching.IQCaching
{
    [QCaching.QCaching(AbsoluteExpiration = 10)]
    public virtual string GetCurrentUtcTime()
    {
        return System.DateTime.UtcNow.ToString();
    }
}

在控制器中,就不是簡單的實例化一下這個BLL的對象就好了,還須要借肋ILifetimeScope去Resolve。若是是直接實例化的話,是沒辦法攔截到的。

public class BllController : Controller
{
    private ILifetimeScope _scope;
    private DateTimeBLL _dateTimeBLL;

    public BllController(ILifetimeScope scope)
    {
        this._scope = scope;
        _dateTimeBLL = _scope.Resolve<DateTimeBLL>();
    }

    public IActionResult Index()
    {
        return Content(_dateTimeBLL.GetCurrentUtcTime());
    }
}

同時還要在builder中啓用類的攔截EnableClassInterceptors

//scenario 2
builder.RegisterAssemblyTypes(assembly)
             .Where(type => type.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase))
             .EnableClassInterceptors()
             .InterceptedBy(typeof(QCachingInterceptor));

效果以下:

到這裏已經經過Castle和Autofac完成了簡化緩存的操做了。

下面再來看看用AspectCore該如何來實現。

使用AspectCore來實現

AspectCore是由Lemon丶寫的一個基於AOP的框架。

首先仍是要經過Nuget添加一下相應的包。這裏只須要添加兩個就能夠了。

<PackageReference Include="AspectCore.Core" Version="0.2.2" />
<PackageReference Include="AspectCore.Extensions.DependencyInjection" Version="0.2.2" />

用法大同小異,因此後面只講述一下使用上面的不一樣點。

注:我也是下午看了一下做者的博客和一些單元測試代碼寫的下面的示例代碼,但願沒有對你們形成誤導。

首先,第一個不一樣點就是咱們的攔截器。這裏須要去繼承AbstractInterceptor這個抽象類而且要去重寫Invoke方法。

public class QCachingInterceptor : AbstractInterceptor
{
    [FromContainer]
    public ICachingProvider CacheProvider { get; set; }

    public async override Task Invoke(AspectContext context, AspectDelegate next)
    {
        var qCachingAttribute = GetQCachingAttributeInfo(context.ServiceMethod);
        if (qCachingAttribute != null)
        {
            await ProceedCaching(context, next, qCachingAttribute);
        }
        else
        {
            await next(context);
        }
    }
}

細心的讀者會發現,二者並無太大的區別!

緩存的接口,這裏是用FromContainer的形式的處理的。

接下來是Service的不一樣。

這裏主要就是把Attribute放到了接口的方法中,而不是其實現類上面。

public interface IDateTimeService : QCaching.IQCaching
{     
    [QCaching.QCaching(AbsoluteExpiration = 10)]
    string GetCurrentUtcTime();
}

public class DateTimeService : IDateTimeService
{
    //[QCaching.QCaching(AbsoluteExpiration = 10)]
    public string GetCurrentUtcTime()
    {
        return System.DateTime.UtcNow.ToString();
    }
}

而後是使用實例化方式時的控制器也略有不一樣,主要是替換了一下相關的接口,這裏用的是IServiceResolver

public class BllController : Controller
{
    private IServiceResolver _scope;
    private DateTimeBLL _dateTimeBLL;

    public BllController(IServiceResolver scope)
    {
        this._scope = scope;
        _dateTimeBLL = _scope.Resolve<DateTimeBLL>();
    }

    public IActionResult Index()
    {
        return Content(_dateTimeBLL.GetCurrentUtcTime());
    }

最後,也是相當重要的Stratup。

public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddScoped<ICachingProvider, MemoryCachingProvider>();
        services.AddScoped<IDateTimeService, DateTimeService>();

        //handle BLL class
        var assembly = this.GetType().GetTypeInfo().Assembly;
        this.AddBLLClassToServices(assembly, services);

        var container = services.ToServiceContainer();
        container.AddType<QCachingInterceptor>();
        container.Configure(config =>
        {
            config.Interceptors.AddTyped<QCachingInterceptor>(method => typeof(IQCaching).IsAssignableFrom(method.DeclaringType));
        });

        return container.Build();
    }

    public void AddBLLClassToServices(Assembly assembly, IServiceCollection services)
    {
        var types = assembly.GetTypes().ToList();

        foreach (var item in types.Where(x => x.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase) && x.IsClass))
        {
            services.AddSingleton(item);
        }
    }
    
    //other code...
}

我這裏是先用自帶的DependencyInjection完成了一些操做,而後纔去用ToServiceContainer()獲得AspectCore內置容器。

獲得這個容器後,就去配置攔截了。

最終的效果是和前面同樣的,就再也不放圖了。

總結

AOP在某些方面的做用確實很明顯,也很方便,能作的事情也不少。

對比Castle和AspectCore的話,二者各有優勢!

就我我的使用而言,對Castle略微熟悉一下,資料也比較多。

對AspectCore的話,我比較喜歡它的配置,比較簡單,依賴也少。

本文的兩個示例Demo:

CachingAOPDemo

相關文章
相關標籤/搜索