AOP-代理拦截实现Redis缓存

Posted 言00FFCC

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP-代理拦截实现Redis缓存相关的知识,希望对你有一定的参考价值。

使用AOP代理拦截方式实现缓存.

上文简单的缓存实现方式:.Net Core WebAPI 利用 IActionFilter 实现请求缓存 需要将缓存定义在控制器Controller层,增加了对控制器层的耦合度。

另外,缓存的是控制器层面的结果IActionResult缓存。很明显对于复杂逻辑的Action无法做到针对数据层的缓存。

以用户获取信息的例子来解释:

/// <summary>
/// 根据账号密码获取用户信息
/// </summary>
/// <param name="req"> account账号、password密码 (密码传输使用 md5 加密)</param>
/// <returns></returns>
[HttpPost("get_userinfo")]
public async Task<ExecuteResult<UserInfoModel>> GetUserInfo(dynamic req)

	

如用户多次获取人员的信息UserInfoModel人员的信息一般情况下,在10分钟乃至半小时内不会做任何变动。
那么就需要将用户登录第一次查询数据库获取用户信息的结果进行缓存。

假如往下需要进行一个用户登录行为的其他逻辑判断。

当然在此处也可以使用方法拦截方式实现,但不是本章主要内容,忽略其他实现。

那就需要将获取人员的信息 UserInfoModel 进行缓存。

那么本文,将以与Controller层解耦的方式,实现针对业务逻辑的代理缓存方式。

常见的AOP实现有: AutoFac 、AspectCore 等等,本文将基于 AspectCore 微型框架的实现AOP
AspectCore 的简介,在CSDN中已经有多位大牛出过文章了,这里不做过多解释。

  • 项目引入AspectCore
    右键项目打开 NuGet 程序包,搜索并引入 AspectCore.Core,AspectCore.Abstractions 两个包。

注意,AspectCore 拦截是拦截接口方法、或抽象方法,所以需要将拦截标记在接口层。

  • 缓存标记类
    新建CustomCacheAttribute 需要标记在接口方法上,继承Attribute,用来标记该逻辑接口的结果返回值需要进行缓存
 /// <summary>
 /// 自定义缓存
 /// </summary>
 [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
 public class CustomCacheAttribute : Attribute
 
     /// <summary>
     /// 缓存过期时间[分钟] 默认缓存时间30分钟
     /// </summary>
     public int ValidTimeMinutes  get; set; 
 
  • 缓存拦截
    新建类 CacheInterceptorAttribute 继承 AbstractInterceptorAttribute,实现Invoke方法
public override async Task Invoke(AspectContext context, AspectDelegate next)

    await next(context); //执行原有方法体

  • 配置拦截作用
    StartupConfigureServices 中配置
public void ConfigureServices(IServiceCollection services)

	......
	// 注入redis缓存组件
    services.AddDistributedRedisCache(r => r.Configuration = Configuration["Redis:ConnectionString"]);
	//注入全局拦截器
	services.ConfigureDynamicProxy(config =>
	
		// 作用于Service后缀的类中.
	    config.Interceptors.AddTyped<CacheInterceptorAttribute>(Predicates.ForService("*Service"));
	);
	......

另外还需要在 ProgramCreateHostBuilder 中配置拦截器

public static IHostBuilder CreateHostBuilder(string[] args) =>
	Host.CreateDefaultBuilder(args)
	   .ConfigureWebHostDefaults(webBuilder =>
	   
	       webBuilder.UseStartup<Startup>();
	       webBuilder.UseUrls("http://*:15208");
	   )
	   .UseDynamicProxy(); // 注入拦截器

.UseDynamicProxy() 这个注入非常重要,如果未注入,代理器无法启动。

  • CacheInterceptorAttribute 逻辑

回到类CacheInterceptorAttribute 中,既然配置拦截的方法为通过通配符 *Service去匹配的。会代理符合该通配符的类下所有的方法。
所以需要判断该方法是否标记了CustomCacheAttribute,未标记那就不必进行拦截。

另外如果方法连结果都不返回,那么该方法也就无需进行缓存了。

完整代码如下:

using AspectCore.DynamicProxy;
using AspectCore.DynamicProxy.Parameters;
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
public class CacheInterceptorAttribute : AbstractInterceptorAttribute

	public override async Task Invoke(AspectContext context, AspectDelegate next)
	
	    // 获取方法的自定义特性是否标记CustomCacheAttribute
	    var cache = context.GetAttribute<CustomCacheAttribute>();
	    //先判断方法是否有返回值,无就不进行缓存判断
	    var methodReturnType = context.GetReturnParameter().Type;
	    bool isVoidReturn = methodReturnType == typeof(void) || methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask);
	    // 如果已标记CustomCacheAttribute 并且方法带有返回值,则执行缓存操作设置返回值并跳出方法。
	    if (cache != null && !isVoidReturn)
	    
	        Type returnType = context.GetReturnType();
	        var value = await ExecuteCacheLogicAsync(context, next, cache);
	        context.ReturnValue = ResultFactory(value, returnType, context.IsAsync());
	        // 请注意,这里必须return,这里不像IActionResult中设置了context.ReturnValue就会结束方法体.
	        return; 
	    
	    // 执行原来方法的方法体.
	    await next(context);
	
	
	/// <summary>
	/// 缓存逻辑
	/// </summary>
	/// <param name="context"></param>
	/// <param name="next"></param>
	/// <param name="cache"></param>
	/// <returns></returns>
	private async Task<object> ExecuteCacheLogicAsync(AspectContext context, AspectDelegate next, CustomCacheAttribute cache)
	
		// 拼接整个缓存的 key Md5Encrypt是自定义的扩展方法,把string进行md5加密.
	    var cacheKeyBuilder = new StringBuilder();
	    cacheKeyBuilder.Append($"context.ServiceMethod.DeclaringType.FullName.context.ServiceMethod.Name");
	    string cacheParams = (string.Join('|', context.GetParameters()
	       .Select((p, i) => new KeyValuePair<string, object>(p.Name, p.Value))
	       .Select(p => $"#p.Key:p.Value.ConvertJson()")));
	    cacheKeyBuilder.Append($".cacheParams.Md5Encrypt()");
	    string cacheKey = cacheKeyBuilder.ToString();
	
	    return await CacheGetOrSetAsync(cacheKey, context.GetReturnType(), async () =>
	    
	        await next(context);
	        return context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue;
	    , cache.ValidTimeMinutes);
	
	
	/// <summary>
	/// 设置或获取缓存
	/// </summary>
	/// <param name="context"></param>
	/// <param name="key">缓存key</param>
	/// <param name="ValidTimeMinutes">超时时间</param>
	/// <returns></returns>
	private async Task<object> CacheGetOrSetAsync(string key, Type returnType, Func<Task<object>> getResultFunc, int ValidTimeMinutes)
	
		// CustomDIContainer是自定义获取DI注入对象的方法,具体代码请看开篇引用,这里不做重复。
	    var cache = CustomDIContainer.GetSerivce<IDistributedCache>();
	    byte[] cacheBuffer = cache.Get(key);
	    object result;
	    // 先判断对应key中是否存在缓存
	    if (cacheBuffer == null || cacheBuffer.Length == 0)
	    
	    	// 执行参数传进来的委托方法.
	        result = await getResultFunc();
	        // Serialize为自定义的对象转bute[]数组
	        cacheBuffer = result.ConvertJson().Serialize();
	        // 设置缓存的过期时间及其他的配置
	        var options = new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(ValidTimeMinutes));
	        await cache.SetAsync(key, cacheBuffer, options);
	    
	    else
	    
	    	// 存在缓存直接返回缓存信息
	    	// Deserialize将byte[]转换为指定对象 Json2Type自定义的Json字符串转指定的复式类型
	    	// returnType是原方法的返回类型. 这里为了兼容返回类型是带异步的.
	        result = cacheBuffer.Deserialize<string>().Json2Type(returnType);
	    
	    return result;
	
	
	private static readonly ConcurrentDictionary<Type, MethodInfo> TypeofTaskResultMethod = new ConcurrentDictionary<Type, MethodInfo>();
	// 该方法用来序列化返回值的,兼容了异步返回值
	private object ResultFactory(object result, Type returnType, bool isAsync)
	
	    if (isAsync)
	    
	        return TypeofTaskResultMethod
	            .GetOrAdd(returnType, t => typeof(Task)
	            .GetMethods()
	            .First(p => p.Name == "FromResult" && p.ContainsGenericParameters)
	            .MakeGenericMethod(returnType))
	            .Invoke(null, new object[]  result );
	    
	    else
	    
	        return result;
	    
	

另外返回参数的序列化扩展

using AspectCore.DynamicProxy;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;

public static class AspectContextExtension

    private static readonly ConcurrentDictionary<MethodInfo, object[]> MethodAttributes = new ConcurrentDictionary<MethodInfo, object[]>();

    public static Type GetReturnType(this AspectContext context)
    
        return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
    

    public static T GetAttribute<T>(this AspectContext context) where T : Attribute
    
        MethodInfo method = context.ServiceMethod;
        var attributes = MethodAttributes.GetOrAdd(method, method.GetCustomAttributes(true));
        var attribute = attributes.FirstOrDefault(x => typeof(T).IsAssignableFrom(x.GetType()));
        if (attribute is T t) return t;
        return null;
    

  • 使用方式

网上有说Action方法要为virtual才能成功拦截,其实是错误的。

public interface IOrganizationService

    /// <summary>
    /// 根据账号和密码获取人员信息.
    /// </summary>
    /// <param name="account"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    [CustomCache(ValidTimeMinutes = 10)] // 在这里进行标记 
    Task<Tuple<bool, string, UserInfoModel>> GetUserInfoAsync(string account, string password);

    /// <summary>
    /// 获取系统的组织架构
    /// </summary>
    /// <returns></returns>
    [CustomCache(ValidTimeMinutes = 60 * 24)]  // 在这里进行标记
    Task<Dictionary<string, Dictionary<string, List<UserInfoModel>>>> GetOrganizationAsync();

至此对应的接口实现类就会自动缓存结果了。

最后,我希望以最详细的解释去介绍如何实现,如果在本文中有错漏之处,请各位大佬们指正,,如果我的文章能帮到你,也请各位不吝点个赞点个收藏,如果对文中代码有疑问,也请下方评论。谢谢各位看官。

以上是关于AOP-代理拦截实现Redis缓存的主要内容,如果未能解决你的问题,请参考以下文章

AOP-代理拦截实现Redis缓存

再话AOP,从简化缓存操作说起

再话AOP,从简化缓存操作说起

Spring通过AOP实现对Redis的缓存同步

JAVA动态代理和方法拦截(使用CGLib实现AOP)

Spring aop 拦截不到Dao