关于缓存的使用,相信你们都是熟悉的不能再熟悉了,简单来讲就是下面一句话。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来进行演示。
这里主要是作了作了两件事
下面就先从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(); } } }
有两点要注意:
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)); } }
这个方法,就是和大部分操做缓存的代码同样的写法了!
注意下面几个地方
invocation.Proceed()
表示执行当前的方法invocation.ReturnValue
是要执行后才会有值的。下面来看看生成缓存键的操做。
这里生成的依据是当前执行方法的名称,参数以及该方法所在的类名。
生成的代码以下:
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; } }
下面就是怎么用的问题了。
这里考虑了两种用法:
先来看看面向接口的用法
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是由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: