在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

posted @ 2017-11-06 08:37  Catcher8  阅读(3731)  评论(18编辑  收藏  举报