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


前言

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

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

然后我們就會看到項目中有類似這樣的代碼了。

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);
}

隨着業務的不斷發展,可能會出現越來越多類似第一段的示例代碼。這樣就會出現大量“重復的代碼”了!

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

基於這樣的情景下,我們完全可以使用AOP去簡化緩存這一部分的代碼。

大致的思路如下 :

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

如果緩存中沒有的話,就是去執行這個方法,拿到返回值,執行完成之后,把對應的數據寫到緩存中去,

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

本文分別使用了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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM