在ASP.NET Core中使用AOP来简化缓存操作
前言
關(guān)于緩存的使用,相信大家都是熟悉的不能再熟悉了,簡(jiǎn)單來說就是下面一句話。
優(yōu)先從緩存中取數(shù)據(jù),緩存中取不到再去數(shù)據(jù)庫(kù)中取,取到了在扔進(jìn)緩存中去。
然后我們就會(huì)看到項(xiàng)目中有類似這樣的代碼了。
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; }
然而在初期,沒有緩存的時(shí)候,可能這個(gè)方法就一行代碼。
public Product Get(int productId){ ? ?return Query(productId); }隨著業(yè)務(wù)的不斷發(fā)展,可能會(huì)出現(xiàn)越來越多類似第一段的示例代碼。這樣就會(huì)出現(xiàn)大量“重復(fù)的代碼”了!
顯然,我們不想讓這樣的代碼到處都是!
基于這樣的情景下,我們完全可以使用AOP去簡(jiǎn)化緩存這一部分的代碼。
大致的思路如下 :
在某個(gè)有返回值的方法執(zhí)行前去判斷緩存中有沒有數(shù)據(jù),有就直接返回了;
如果緩存中沒有的話,就是去執(zhí)行這個(gè)方法,拿到返回值,執(zhí)行完成之后,把對(duì)應(yīng)的數(shù)據(jù)寫到緩存中去,
下面就根據(jù)這個(gè)思路來實(shí)現(xiàn)。
本文分別使用了Castle和AspectCore來進(jìn)行演示。
這里主要是做了做了兩件事
自動(dòng)處理緩存的key,避免硬編碼帶來的坑
通過Attribute來簡(jiǎn)化緩存操作
下面就先從Castle開始吧!
使用Castle來實(shí)現(xiàn)
一般情況下,我都會(huì)配合Autofac來實(shí)現(xiàn),所以這里也不例外。
我們先新建一個(gè)ASP.NET Core 2.0的項(xiàng)目,通過Nuget添加下面幾個(gè)包(當(dāng)然也可以直接編輯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" />然后做一下前期準(zhǔn)備工作
1.緩存的使用
定義一個(gè)ICachingProvider和其對(duì)應(yīng)的實(shí)現(xiàn)類MemoryCachingProvider
簡(jiǎn)化了一下定義,就留下讀和取的操作。
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.定義一個(gè)Attribute
這個(gè)Attribute就是我們使用時(shí)候的關(guān)鍵了,把它添加到要緩存數(shù)據(jù)的方法中,即可完成緩存的操作。
這里只用了一個(gè)絕對(duì)過期時(shí)間(單位是秒)來作為演示。如果有其他緩存的配置,也是可以往這里加的。
[AttributeUsage(AttributeTargets.Method, Inherited = true)]public class QCachingAttribute : Attribute{ ?
? ??public int AbsoluteExpiration { get; set; } = 30; ? ?//add other settings ...}
3.定義一個(gè)空接口
這個(gè)空接口只是為了做一個(gè)標(biāo)識(shí)的作用,為了后面注冊(cè)類型而專門定義的。
public interface IQCaching{ }4.定義一個(gè)與緩存鍵相關(guān)的接口
定義這個(gè)接口是針對(duì)在方法中使用了自定義類的時(shí)候,識(shí)別出這個(gè)類對(duì)應(yīng)的緩存鍵。
public interface IQCachable{ ? ?string CacheKey { get; } }準(zhǔn)備工作就這4步(AspectCore中也是要用到的),
下面我們就是要去做方法的攔截了(攔截器)。
攔截器首先要繼承并實(shí)現(xiàn)IInterceptor這個(gè)接口。
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();}} }
有兩點(diǎn)要注意:
因?yàn)橐褂镁彺?#xff0c;所以這里需要我們前面定義的緩存操作接口,并且在構(gòu)造函數(shù)中進(jìn)行注入。
Intercept方法是攔截的關(guān)鍵所在,也是IInterceptor接口中的唯一定義。
Intercept方法其實(shí)很簡(jiǎn)單,獲取一下當(dāng)前執(zhí)行方法是不是有我們前面自定義的QCachingAttribute,有的話就去處理緩存,沒有的話就是僅執(zhí)行這個(gè)方法而已。
下面揭開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));} }
這個(gè)方法,就是和大部分操作緩存的代碼一樣的寫法了!
注意下面幾個(gè)地方
invocation.Proceed()表示執(zhí)行當(dāng)前的方法
invocation.ReturnValue是要執(zhí)行后才會(huì)有值的。
在每次執(zhí)行前,都會(huì)依據(jù)當(dāng)前執(zhí)行的方法去生成一個(gè)緩存的鍵。
下面來看看生成緩存鍵的操作。
這里生成的依據(jù)是當(dāng)前執(zhí)行方法的名稱,參數(shù)以及該方法所在的類名。
生成的代碼如下:
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(); }//處理方法的參數(shù),可根據(jù)情況自行調(diào)整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這個(gè)方法,因?yàn)橐粋€(gè)方法的參數(shù)有可能是基本的數(shù)據(jù)類型,也有可能是自己定義的類。
對(duì)于自己定義的類,必須要去實(shí)現(xiàn)IQCachable這個(gè)接口,并且要定義好鍵要取的值!
如果說,在一個(gè)方法的參數(shù)中,有一個(gè)自定義的類,但是這個(gè)類卻沒有實(shí)現(xiàn)IQCachable這個(gè)接口,那么生成的緩存鍵將不會(huì)包含這個(gè)參數(shù)的信息。
舉個(gè)生成的例子:
MyClass:MyMethod:100:abc:999到這里,我們緩存的攔截器就已經(jīng)完成了。
下面是刪除了注釋的代碼(可去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;} } ?
下面就是怎么用的問題了。
這里考慮了兩種用法:
-
一種是面向接口的用法,也是目前比較流行的用法
-
一種是傳統(tǒng)的,類似通過實(shí)例化一個(gè)BLL層對(duì)象的方法。
先來看看面向接口的用法
public interface IDateTimeService{ ? ? ? ?string GetCurrentUtcTime(); }public class DateTimeService : IDateTimeService, QCaching.IQCaching{[QCaching.QCaching(AbsoluteExpiration = 10)] ?
?public string GetCurrentUtcTime() ? ?{ ?
?? ? ?return System.DateTime.UtcNow.ToString();} }
簡(jiǎn)單起見,就返回當(dāng)前時(shí)間了,也是看緩存是否生效最簡(jiǎn)單有效的辦法。
在控制器中,我們只需要通過構(gòu)造函數(shù)的方式去注入我們上面定義的Service就可以了。
public class HomeController : Controller{ ??private IDateTimeService _dateTimeService; ?
?
??public HomeController(IDateTimeService dateTimeService) ? ?{_dateTimeService = dateTimeService;} ? ?
??
??public IActionResult Index() ? ?{ ? ?
??? ?return Content(_dateTimeService.GetCurrentUtcTime());} }
如果這個(gè)時(shí)候運(yùn)行,肯定是會(huì)出錯(cuò)的,因?yàn)槲覀冞€沒有配置!
去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 1builder.RegisterAssemblyTypes(assembly).Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract).AsImplementedInterfaces().InstancePerLifetimeScope().EnableInterfaceInterceptors().InterceptedBy(typeof(QCachingInterceptor)); ? ? ? return new AutofacServiceProvider(builder.Build());} ? ?//other ...}
要注意的是這個(gè)方法原來是沒有返回值的,現(xiàn)在需要調(diào)整為返回IServiceProvider。
這段代碼,網(wǎng)上其實(shí)有很多解釋,這里就不再細(xì)說了,主要是EnableInterfaceInterceptors和InterceptedBy。
下面是運(yùn)行的效果:
再來看看通過實(shí)例化的方法
先定義一個(gè)BLL層的方法,同樣是返回當(dāng)前時(shí)間。這里我們直接把Attribute放到這個(gè)方法中即可,同時(shí)還要注意是virtual的。
public class DateTimeBLL : QCaching.IQCaching{[QCaching.QCaching(AbsoluteExpiration = 10)] ??public virtual string GetCurrentUtcTime() ? ?{ ? ?
? ? ?return System.DateTime.UtcNow.ToString();} }
在控制器中,就不是簡(jiǎn)單的實(shí)例化一下這個(gè)BLL的對(duì)象就行了,還需要借肋ILifetimeScope去Resolve。如果是直接實(shí)例化的話,是沒辦法攔截到的。
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());} }
同時(shí)還要在builder中啟用類的攔截EnableClassInterceptors
//scenario 2builder.RegisterAssemblyTypes(assembly).Where(type => type.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase)).EnableClassInterceptors().InterceptedBy(typeof(QCachingInterceptor));效果如下:
到這里已經(jīng)通過Castle和Autofac完成了簡(jiǎn)化緩存的操作了。
下面再來看看用AspectCore該如何來實(shí)現(xiàn)。
使用AspectCore來實(shí)現(xiàn)
AspectCore是由Lemon丶寫的一個(gè)基于AOP的框架。
首先還是要通過Nuget添加一下相應(yīng)的包。這里只需要添加兩個(gè)就可以了。
<PackageReference Include="AspectCore.Core" Version="0.2.2" /><PackageReference Include="AspectCore.Extensions.DependencyInjection" Version="0.2.2" />用法大同小異,所以后面只講述一下使用上面的不同點(diǎn)。
注:我也是下午看了一下作者的博客和一些單元測(cè)試代碼寫的下面的示例代碼,希望沒有對(duì)大家造成誤導(dǎo)。
首先,第一個(gè)不同點(diǎn)就是我們的攔截器。這里需要去繼承AbstractInterceptor這個(gè)抽象類并且要去重寫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);}} }
細(xì)心的讀者會(huì)發(fā)現(xiàn),兩者并沒有太大的區(qū)別!
緩存的接口,這里是用FromContainer的形式的處理的。
接下來是Service的不同。
這里主要就是把Attribute放到了接口的方法中,而不是其實(shí)現(xiàn)類上面。
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();} }
然后是使用實(shí)例化方式時(shí)的控制器也略有不同,主要是替換了一下相關(guān)的接口,這里用的是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());}
最后,也是至關(guān)重要的Stratup。
public class Startup{ ??public IServiceProvider ConfigureServices(IServiceCollection services) ? ?{services.AddMvc();services.AddScoped<ICachingProvider, MemoryCachingProvider>();services.AddScoped<IDateTimeService, DateTimeService>(); ? ? ? ?//handle BLL classvar 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內(nèi)置容器。
得到這個(gè)容器后,就去配置攔截了。
最終的效果是和前面一樣的,就不再放圖了。
總結(jié)
AOP在某些方面的作用確實(shí)很明顯,也很方便,能做的事情也很多。
對(duì)比Castle和AspectCore的話,兩者各有優(yōu)點(diǎn)!
就我個(gè)人使用而言,對(duì)Castle略微熟悉一下,資料也比較多。
對(duì)AspectCore的話,我比較喜歡它的配置,比較簡(jiǎn)單,依賴也少。
原文地址:http://www.cnblogs.com/catcher1994/p/7788890.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號(hào)文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的在ASP.NET Core中使用AOP来简化缓存操作的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 腾讯云短信服务使用记录与.NET Cor
- 下一篇: .NET Core跨平台的奥秘[上篇]: