在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来进行演示。
这里主要是做了做了两件事
- 自动处理缓存的key,避免硬编码带来的坑
- 通过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();
}
}
}
有两点要注意:
- 因为要使用缓存,所以这里需要我们前面定义的缓存操作接口,并且在构造函数中进行注入。
- 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));
}
}
这个方法,就是和大部分操作缓存的代码一样的写法了!
注意下面几个地方
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;
}
}
下面就是怎么用的问题了。
这里考虑了两种用法:
- 一种是面向接口的用法,也是目前比较流行的用法
- 一种是传统的,类似通过实例化一个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来实现
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:
在ASP.NET Core中使用AOP来简化缓存操作的更多相关文章
- [译]如何在ASP.NET Core中实现面向切面编程(AOP)
原文地址:ASPECT ORIENTED PROGRAMMING USING PROXIES IN ASP.NET CORE 原文作者:ZANID HAYTAM 译文地址:如何在ASP.NET Cor ...
- ASP.NET Core教程:ASP.NET Core中使用Redis缓存
参考网址:https://www.cnblogs.com/dotnet261010/p/12033624.html 一.前言 我们这里以StackExchange.Redis为例,讲解如何在ASP.N ...
- ASP.NET Core中使用IOC三部曲(三.采用替换后的Autofac来实现AOP拦截)
前言 本文主要是详解一下在ASP.NET Core中,采用替换后的Autofac来实现AOP拦截 觉得有帮助的朋友~可以左上角点个关注,右下角点个推荐 这里就不详细的赘述IOC是什么 以及DI是什么了 ...
- ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器)
前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ...
- ASP.NET Core中使用IOC三部曲(二.采用Autofac来替换IOC容器,并实现属性注入)
前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ...
- ASP.NET Core中Middleware的使用
https://www.cnblogs.com/shenba/p/6361311.html ASP.NET 5中Middleware的基本用法 在ASP.NET 5里面引入了OWIN的概念,大致意 ...
- 第十五节:Asp.Net Core中的各种过滤器(授权、资源、操作、结果、异常)
一. 简介 1. 说明 提到过滤器,通常是指请求处理管道中特定阶段之前或之后的代码,可以处理:授权.响应缓存(对请求管道进行短路,以便返回缓存的响应). 防盗链.本地化国际化等,过滤器用于横向处理业务 ...
- ASP.NET Core 中的那些认证中间件及一些重要知识点
前言 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系列(一,二,三)奠定一下基础. 有关于 Authentication 的知识太广,所以本篇介绍几个在 A ...
- Asp.net Core中使用Session
前言 2017年就这么悄无声息的开始了,2017年对我来说又是特别重要的一年. 元旦放假在家写了个Asp.net Core验证码登录, 做demo的过程中遇到两个小问题,第一是在Asp.net Cor ...
随机推荐
- oracle pl/sql 包
包用于在逻辑上组合过程和函数,它由包规范和包体两部分组成.1).我们可以使用create package命令来创建包,如:i.创建一个包sp_packageii.声明该包有一个过程update_sal ...
- AngularJS 1.3中的一次性数据绑定(one-time bindings)
点击查看AngularJS系列目录 谈谈AngularJS 1.3中的一次性数据绑定(one-time bindings) 不久之前,AngularJS 1.3版本正式发布,其中添加了很多的性特性,同 ...
- PyTorch教程之Training a classifier
我们已经了解了如何定义神经网络,计算损失并对网络的权重进行更新. 接下来的问题就是: 一.What about data? 通常处理图像.文本.音频或视频数据时,可以使用标准的python包将数据加载 ...
- class DELPHICLASS TObject
class DELPHICLASS TObject 1.自己猜想:delphi,是windows平台的快速应用程序开发工具Rapid Application Development 简称RAD. ...
- http://codeforces.com/problemset/problem/594/A
A. Warrior and Archer time limit per test 2 seconds memory limit per test 256 megabytes input standa ...
- httpd网页身份认证
html { font-family: sans-serif } body { margin: 0 } article,aside,details,figcaption,figure,footer,h ...
- jquery定时刷新数据
$(function () { setInterval("startRequest()", 3000); }); function startRequest() { $(" ...
- 在ubuntu16.04中安装apache2+modsecurity以及自定义WAF规则详解
一.Modsecurity规则语法示例 SecRule是ModSecurity主要的指令,用于创建安全规则.其基本语法如下: SecRule VARIABLES OPERATOR [ACTIONS] ...
- Java集合框架,未完
一.集合类 集合的由来: 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就需要将对象进行存储,集合就是存储对象最常用的一种方式. 集合特点:1,用于存储对象的容器.(容器本身就是 ...
- Python+OpenCV图像处理(一)
Python+OpenCV图像处理(一): 读取,写入和展示图片 调用摄像头拍照 调用摄像头录制视频 1. 读取.写入和展示图片 图像读入:cv2.imread() 使用函数cv2.imread() ...