自定义特性+AOP实现缓存
2018-08-21 22:37:301. 目标
如下代码:我们要实现缓存,但希望让使用者不用关心缓存的具体实现,只需要使用者在要操作缓存的方法上加上特性标注即可。
[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>();
到此我们就完成了缓存的特性注入,使用时即可像目标中的代码形式那样注入我们的缓存调用。
思考
移除缓存传入的是一个可变参数,如果我们在更新操作后需要移除的缓存很多时,可以在特性中实现一个通配符的实现逻辑,这个实现就留给读者了。