Spiga

自定义特性+AOP实现缓存

2018-08-21 22:37:30

1. 目标

如下代码:我们要实现缓存,但希望让使用者不用关心缓存的具体实现,只需要使用者在要操作缓存的方法上加上特性标注即可。

[Caching(CachingMethod.Remove, "GetLinksQuery")]
public class CreateLinkCommand
{
    
}
​
[Caching(CachingMethod.Get)]
public class GetLinksQuery : IRequest<List<LinkViewModel>>
{
    
}

要实现我们的目标,我们把任务分成2部分,首先实现缓存逻辑,然后将缓存基于特性做AOP实现。

2. 缓存实现

首先我们定义一个缓存接口

public interface ICacheProvider
{
    /// <summary>
    /// 向缓存中添加一个对象。
    /// </summary>
    /// <param name="key">缓存的键值,该值通常是使用缓存机制的方法的名称。</param>
    /// <param name="valKey">缓存值的键值,该值通常是由使用缓存机制的方法的参数值所产生。</param>
    /// <param name="value">需要缓存的对象。</param>
    void Add(string key, string valKey, object value);
    void Put(string key, string valKey, object value);
    object Get(string key, string valKey);
    void Remove(string key);
    bool Exists(string key);
    bool Exists(string key, string valKey);
}

如上代码,为什么接口中key和valKey2个参数呢?这是因为我们可能会缓存同一个方法不同参数的结果,如在一个获取分页结果的方法中,我们可能会返回不同页的结果。如我们目标中GetLinksQuery方法缓存的值会是一个分页显示结果的字典。key是我们缓存的方法名,valKey这是缓存的字典结果中的字典key,value则是字典的结果。要进一步理解可以查看下面基于内存的默认实现:

public class DefaultCacheProvider : ICacheProvider
{
    private readonly IMemoryCache _cacheManager;
​
    public DefaultCacheProvider(IMemoryCache memoryCache)
    {
        _cacheManager = memoryCache;
    }
​
    public void Add(string key, string valKey, object value)
    {
        if (_cacheManager.TryGetValue(key, out Dictionary<string, object> dict))
        {
            dict[valKey] = value;
            _cacheManager.Set(key, dict);
        }
        else
        {
            dict = new Dictionary<string, object>();
            dict.Add(valKey, value);
            _cacheManager.Set(key, dict);
        }
    }
​
    public void Put(string key, string valKey, object value)
    {
        Add(key, valKey, value);
    }
​
    public object Get(string key, string valKey)
    {
        if (_cacheManager.TryGetValue(key, out Dictionary<string, object> dict))
        {
            if (dict.ContainsKey(valKey))
                return dict[valKey];
            else
                return null;
        }
        return null;
    }
​
    public void Remove(string key)
    {
        _cacheManager.Remove(key);
    }
​
    public bool Exists(string key)
    {
        return _cacheManager.TryGetValue(key, out Dictionary<string, object> dict);
    }
​
    public bool Exists(string key, string valKey)
    {
        if (_cacheManager.TryGetValue(key, out Dictionary<string, object> dict))
        {
            return dict.ContainsKey(valKey);
        }
        return false;
    }
}

缓存管理对象_cacheManager里面存储的是一个Dictionary<string, object>的数组,valKey则是这个字典的key。

3、特性的定义

特性我们可以定义在方法或者类上,我们例子因为采用的cqrs模式来实现的业务,操作和查询是一个类实例的handler来实现的,所以文章中的特性基于class来定义。 如果你不是使用的cqrs,而是IService这种服务接口来实现业务,那特性可以定义在服务类的具体方法上。定义特性代码如下:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class CachingAttribute : Attribute
{
    /// <summary>
    /// 初始化一个新的<c>CachingAttribute</c>类型。
    /// </summary>
    /// <param name="method">缓存方式。</param>
    public CachingAttribute(CachingMethod method)
    {
        this.Method = method;
    }
    /// <summary>
    /// 初始化一个新的<c>CachingAttribute</c>类型。
    /// </summary>
    /// <param name="method">缓存方式。</param>
    /// <param name="cacheKey">key。</param>
    /// <param name="correspondingMethodNames">与当前缓存方式相关的方法名称。注:此参数仅在缓存方式为Remove时起作用。</param>
    public CachingAttribute(CachingMethod method, params string[] correspondingMethodNames)
        : this(method)
    {
        this.CorrespondingMethodNames = correspondingMethodNames;
    }
​
    /// <summary>
    /// 获取或设置缓存方式。
    /// </summary>
    public CachingMethod Method { get; set; }
​
    /// <summary>
    /// 获取或设置一个<see cref="Boolean"/>值,该值表示当缓存方式为Put时,是否强制将值写入缓存中。
    /// </summary>
    public bool Force { get; set; }
    /// <summary>
    /// 获取或设置与当前缓存方式相关的方法名称。注:此参数仅在缓存方式为Remove时起作用。
    /// </summary>
    public string[] CorrespondingMethodNames { get; set; }
}

特性类的构造函数中需要传递一个CachingMethod参数,这个参数是一个枚举值,用来指定我们对缓存的操作是存在、还是更新、或者是移除。CachingMethod代码如下:

public enum CachingMethod
{
    /// <summary>
    /// 表示需要从缓存中获取对象。如果缓存中不存在所需的对象,系统则会调用实际的方法获取对象,
    /// 然后将获得的结果添加到缓存中。
    /// </summary>
    Get,
    /// <summary>
    /// 表示需要将对象存入缓存。此方式会调用实际方法以获取对象,然后将获得的结果添加到缓存中,
    /// 并直接返回方法的调用结果。
    /// </summary>
    Put,
    /// <summary>
    /// 表示需要将对象从缓存中移除。
    /// </summary>
    Remove
}

4、实现AOP

最后就是实现AOP,由于我们cqrs是基于MediatR来实现的,因此我们可以直接继承IPipelineBehavior<TRequest, TResponse>来实现AOP。

public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ICacheProvider _cacheProvider;
​
    public CachingBehavior(ICacheProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }
​
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        TResponse result;
        var attributes = request.GetType().GetCustomAttributes(typeof(CachingAttribute), false);
        if (attributes.Any())
        {
            var cache = (CachingAttribute)attributes[0];
            var key = request.GetGenericTypeName();
            // 获取参数
            var valKey = GetValueKey(cache, request.GetType().GetProperties(), request);
​
            switch (cache.Method)
            {
                case CachingMethod.Get:
                    // 3、是否存在缓存
                    if (_cacheProvider.Exists(key, valKey))
                    {
                        // 4、从缓存返回
                        return (TResponse)_cacheProvider.Get(key, valKey);
                    }
                    else
                    {
                        // 4.1 执行方法
                        result = await next();
​
                        // 4.2缓存执行结果
                        _cacheProvider.Add(key, valKey, result);
                        return result;
                    }
                case CachingMethod.Put:
                    // 3 执行方法
                    result = await next();
                    // 4、是否存在缓存
                    if (_cacheProvider.Exists(key))
                    {
                        // 该值表示当缓存方式为Put时,是否强制将值写入缓存中
                        if (cache.Force)
                        {
                            // 4.1 更新缓存结果
                            _cacheProvider.Remove(key);
                            _cacheProvider.Add(key, valKey, result);
                        }
                        else
                            _cacheProvider.Put(key, valKey, result);
                    }
                    else
                    {
                        // 4.1 新增缓存结果
                        _cacheProvider.Add(key, valKey, result);
                    }
                    return result;
                case CachingMethod.Remove:
                    // 3、删除缓存
                    var removeKeys = cache.CorrespondingMethodNames;
                    foreach (var removeKey in removeKeys)
                    {
                        if (_cacheProvider.Exists(removeKey))
                            _cacheProvider.Remove(removeKey);
                    }
                    // 4 执行方法
                    result = await next();
                    return result;
            }
        }
        return await next();
    }
​
    private string GetValueKey(CachingAttribute cache, PropertyInfo[] info, TRequest request)
    {
        switch (cache.Method)
        {
            // 如果是Remove,则不存在特定值键名,所有的以该方法名称相关的缓存都需要清除
            case CachingMethod.Remove:
                return null;
            // 如果是Get或者Put,则需要产生一个针对特定参数值的键名
            case CachingMethod.Get:
            case CachingMethod.Put:
                if (info != null &&
                    info.Length > 0)
                {
                    var sb = new StringBuilder();
                    for (int i = 0; i < info.Length; i++)
                    {
                        sb.Append(info[i].GetValue(request));
                        if (i != info.Length - 1)
                            sb.Append("_");
                    }
                    return sb.ToString();
                }
                else
                    return "NULL";
            default:
                throw new InvalidOperationException("无效的缓存方式。");
        }
    }
}

5、使用

在setup中注入

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));  
​
services.AddDistributedMemoryCache(); 
services.AddSingleton<ICacheProvider, DefaultCacheProvider>();

到此我们就完成了缓存的特性注入,使用时即可像目标中的代码形式那样注入我们的缓存调用。

思考

移除缓存传入的是一个可变参数,如果我们在更新操作后需要移除的缓存很多时,可以在特性中实现一个通配符的实现逻辑,这个实现就留给读者了。