在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 ...
随机推荐
- python基础之七种运算符
废话不多说,上节说的是数据类型,本篇讲讲数据运算. 在算式"1+2"中,"1"和"2"被称为操作数,"+"被称为运算符 ...
- 【京东详情页】——原生js爬坑之放大镜
一.引言 在商城的详情页中,放大镜的功能是很常见的.这里京东详情页就要做一个仿放大镜的效果,预览如下: 二.实现原理 实际上,放大镜的实现是单纯用几个div,鼠标移入其中一个小图div,触发事件显示另 ...
- Android Framework 初探
最近工作任务不忙,学习一下Android Framework方面的知识. 一.介绍,是什么 Android的Framework是直接应用之下的一层,叫做应用程序框架层.这一层是核心应用程序所使用的AP ...
- springmvc返回枚举属性值
使用fastJSON ,在枚举中写toString 方法 如下@Overridepublic String toString() {return "{" + this.name() ...
- appium启动运行log分析
1.手动启动appium 服务 > Launching Appium server with command: C:\Program Files (x86)\Appium\node.exe ...
- 【POJ】2348 Euclid's Game(扩欧)
Description Two players, Stan and Ollie, play, starting with two natural numbers. Stan, the first pl ...
- java 学习笔记 读取配置文件的三种方式
package com.itheima.servlet.cfg; import java.io.FileInputStream; import java.io.FileNotFoundExceptio ...
- js根据时间戳倒计时
今天有个需求,要在页面上做当前时间距离下个月1号的倒计时.在网上找了很多案例也试了很多,大部分都是获取本地当前时间,然后设置结束时间进行计算,然后倒计时.但是有几个问题: 1.如果本地时间和服务器时间 ...
- Android 添加水印, 以及屏幕存取
/** * 获取水印 */ try { String s = CustomPreferences.getUserInfo().getAccountNo().substring(7,11); View ...
- 2017年十大奇葩画风的H5页面案例,原来脑洞可以这样大
每个人都是视觉动物,画面精美.体验奇特的H5,用户在内心一般都会满分打出,毫不吝啬,同时也毫不犹豫分享,因为此时的分享不掉价儿~ 今天给大家准备了十支H5,画风超级奇特,非常值得一看所有案例均可在19 ...