<h2 id="前言">前言
关于缓存的使用,相信大家都是熟悉的不能再熟悉了,简单来说就是下面一句话。
优先从缓存中取数据,缓存中取不到再去数据库中取,取到了在扔进缓存中去。
然后我们就会看到项目中有类似这样的代码了。
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来进行演示。
这里主要是做了做了两件事
- 自动处理缓存的key,避免硬编码带来的坑
- 通过Attribute来简化缓存操作
下面就先从Castle开始吧!
一般情况下,我都会配合Autofac来实现,所以这里也不例外。
我们先新建一个ASP.NET Core 2.0的项目,通过Nuget添加下面几个包(当然也可以直接编辑csproj来完成的)。
然后做一下前期准备工作
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,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(); } }
}
有两点要注意:
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)); }
}
注意下面几个地方
下面来看看生成缓存键的操作。
这里生成的依据是当前执行方法的名称,参数以及该方法所在的类名。
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,IListparameters)
{
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
FormatArgumentsToPartOfCacheKey(IList 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
到这里,我们缓存的拦截器就已经完成了。
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,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,methodArguments); } private string GenerateCacheKey(string typeName,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。
这段代码,网上其实有很多解释,这里就不再细说了,主要是EnableInterfaceInterceptors和InterceptedBy。
下面是运行的效果:
再来看看通过实例化的方法
先定义一个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是由写的一个基于AOP的框架。
首先还是要通过Nuget添加一下相应的包。这里只需要添加两个就可以了。
用法大同小异,所以后面只讲述一下使用上面的不同点。
首先,第一个不同点就是我们的拦截器。这里需要去继承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()); }</code></pre>
最后,也是至关重要的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:
原文链接:https://www.f2er.com/netcore/71320.html