接续上文(java 实现的数据查询缓存通用模型——缓存清除cacheEvict(2))
4、缓存模型核心AOP实现
4.1 概述
在上两篇文章中,我们主要分析了 @DawnCacheable 实现的缓存,和@DawnCacheEvict实现的缓存清除,之前是从模型上分析的,还没有介绍如何实现这个模型,这篇文章就是分析这两个注解背后的实现。
Spring拥有两个核心,一个是IOC,控制反转和依赖注入,还有一个就是AOP,面向切面编程。在我这几年的编程生涯中,确实也意味到了,AOP是大部分框架的实现核心。AOP配合注解可以实现很优雅的架构设计。
对于DAWN的缓存模型,我们设计了三个注解,因此切点Point Cut也需要三个:
@Pointcut("@annotation(cn.hengyumo.dawn.core.cache.core.DawnCacheable)")
public void dawnCacheablePointCut() {}
@Pointcut("@annotation(cn.hengyumo.dawn.core.cache.core.DawnCacheEvict)")
public void dawnCacheEvictPointCut() {}
@Pointcut("@annotation(cn.hengyumo.dawn.core.cache.core.DawnCacheEvicts)")
public void dawnCacheEvictsPointCut() {}
4.2 DawnCacheable切面
切面我们选用了@Around,因为它可以操作目标方法获取其返回值,并且在执行方法和执行方法之后都可以加入自己想做的事情:
@Around(value = "dawnCacheablePointCut()")
public Object aroundDawnCacheablePointCut(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
DawnCacheable dawnCacheable = AnnotationUtils.getAnnotation(method, DawnCacheable.class);
// 因为AOP拦截的就是@DawnCacheable 所以肯定能取到这个注解,以下这行是避开IDEA的判空警告
Assert.notNull(dawnCacheable, "never null");
log.debug("注解式拦截" + dawnCacheable.toString());
// 生成Key
String key = getDawnCacheKeyCreator(dawnCacheable.cacheKeyCreator())
.createCacheKey(getRequest(), proceedingJoinPoint.getTarget(),
signature.getMethod(), getParams(proceedingJoinPoint));
Object result;
// 如果Key 不为空,则走入缓存流程
if (StringUtil.isNotEmpty(key)) {
// 如果需要同步,则加锁
if (dawnCacheable.sync()) {
synchronized (key.intern()) {
// 获取缓存或者执行方法并写入缓存
result = getFromCacheOrSearch(dawnCacheable, key, proceedingJoinPoint);
}
} else {
result = getFromCacheOrSearch(dawnCacheable, key, proceedingJoinPoint);
}
return result;
}
return proceedingJoinPoint.proceed();
}
@Around 获取的是 ProceedingJoinPoint类型的切点,可以通过MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
获取到切点的签名。通过proceedingJoinPoint.proceed()
执行方法。
synchronized (key.intern())
加锁的目的是为了使得同一时间只有一个请求可以执行真查询,而其他请求需要等这个请求执行完之后去读缓存,这个设计的解释在java 实现的数据查询缓存通用模型(1)——概述和注解设计做了描述,需要的可以去看看。
getFromCacheOrSearch
首先尝试从缓存读取数据,如果没有读到则会从数据库进行查询。此处设计可能会有缓存穿透的风险。在实际方法中,如果返回的是空,那么尽量别返回null,而返回返回空列表、空字符串。
/**
* 从缓存中取数据或者执行查询写入缓存
*
* @param dawnCacheable 注解配置
* @param key 缓存的键
* @param proceedingJoinPoint 切点
* @return 结果
* @throws Throwable 切点proceed原样抛出
*/
private Object getFromCacheOrSearch(DawnCacheable dawnCacheable, String key,
ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object result = getFromCache(key, dawnCacheable.cacheType());
if (result == null) {
result = proceedingJoinPoint.proceed();
long timeToLiveSeconds = dawnCacheable.cacheTimeUnit().toSeconds(dawnCacheable.expire());
setCache(key, result, timeToLiveSeconds, dawnCacheable.cacheType());
} else {
log.info("缓存命中:" + key);
}
return result;
}
getFromCache
从缓存工厂中获取缓存组件,然后根据Key获取缓存:
private Object getFromCache(String key, DawnCacheType dawnCacheType) {
return dawnCacheFactory.getCache(dawnCacheType).getFromCache(key);
}
为了对付缓存穿透的可能,后续可以考虑的优化是,将 Object getFromCache(String key);
这个缓存获取方法增加一个状态标识:
/**
* 从缓存中取值
*
* @param key 键
* @return Pair<Boolean, Optional<?>>, Boolean是标识位,标识是否缓存中存在这个Key
* 该设计是为了避免空值时出现缓存穿透的可能
* Optional 包装是因为Pair内的值都不能为null
*/
Pair<Boolean, Optional<?>> getFromCache(String key);
getFromCacheOrSearch
方法利用这个bool标识,如果是true,那么缓存中是存在这个Key的。通过这样避免了缓存穿透的可能。
private Object getFromCacheOrSearch(DawnCacheable dawnCacheable, String key,
ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Pair<Boolean, Optional<?>> result = getFromCache(key, dawnCacheable.cacheType());
Boolean cacheContainsKey = result.getFirst();
Assert.notNull(cacheContainsKey, "never null");
Object obj = result.getSecond().orElse(null);
if (!cacheContainsKey) {
obj = proceedingJoinPoint.proceed();
long timeToLiveSeconds = dawnCacheable.cacheTimeUnit().toSeconds(dawnCacheable.expire());
setCache(key, obj, timeToLiveSeconds, dawnCacheable.cacheType());
} else {
log.info("缓存命中:" + key);
}
return obj;
}
4.3 DawnCacheEvict切面
DawnCacheEvict切面拦截拥有@DawnCacheEvict注解的方法,其主要功能是在方法执行之后根据缓存清除规则配置的清除缓存。
@Around(value = "dawnCacheEvictPointCut()")
public Object aroundDawnCacheEvictPointCut(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
DawnCacheEvict dawnCacheEvict = AnnotationUtils.getAnnotation(method, DawnCacheEvict.class);
Assert.notNull(dawnCacheEvict, "never null");
log.debug("注解式拦截" + dawnCacheEvict.toString());
Object result = proceedingJoinPoint.proceed();
handleDawnCacheEvict(proceedingJoinPoint, dawnCacheEvict);
return result;
}
handleDawnCacheEvict
方法根据配置执行缓存清除:
private void handleDawnCacheEvict(ProceedingJoinPoint proceedingJoinPoint,
DawnCacheEvict dawnCacheEvictParam) {
Class<?> target = proceedingJoinPoint.getTarget().getClass();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
String name = dawnCacheEvictParam.name();
DawnCacheEvict[] dawnCacheEvicts = new DawnCacheEvict[]{dawnCacheEvictParam};
if (StringUtil.isNotEmpty(name)) {
// 根据名称获取对应的缓存清除规则配置,这里这样设计是为了支持别名机制
dawnCacheEvicts = getDawnCacheEvictsForTarget(target, name);
} else if (dawnCacheEvictIsNotConfig(dawnCacheEvictParam)) {
Method method = signature.getMethod();
log.warn("存在无名无配置的缓存清除注解:{}.{}", target.getName(), method.getName());
return;
}
// 遍历
for (DawnCacheEvict dawnCacheEvict : dawnCacheEvicts) {
// 生成要清除的缓存的前缀
String key = getDawnCacheKeyCreator(dawnCacheEvict.cacheKeyCreator())
.createCacheEvictKey(
dawnCacheEvict.method(), getRequest(), proceedingJoinPoint.getTarget(),
signature.getMethod(), getParams(proceedingJoinPoint));
if (StringUtil.isNotEmpty(key)) {
// 执行清除
int delCount = deleteCache(key, dawnCacheEvict.cacheType());
log.info("清除缓存,前缀-" + key + " 数量:" + delCount);
}
}
}
getDawnCacheEvictsForTarget
根据名称获取对应的缓存清除规则配置,这样设计是为了支持别名机制,具体的设计描述可以参看本系列的第二篇java 实现的数据查询缓存通用模型——缓存清除cacheEvict(2)的3.4别名机制。
dawnCacheEvictIsNotConfig
用于判断注解是不是什么属性都没有配置,例如这样的:@DawnCacheEvict
,未配置任何属性的清除规则是不合理的。故而会报出警告,并且不处理。
deleteCache
方法从工厂中获取缓存组件并执行删除缓存的操作。
private int deleteCache(String keyPrefix, DawnCacheType dawnCacheType) {
return dawnCacheFactory.getCache(dawnCacheType).deleteCache(keyPrefix);
}
4.4 DawnCacheEvicts切面
@DawnCacheEvicts可以看作是一个@DawnCacheEvict的列表,所以切面的流程就是遍历它,然后挨个重复4.3的DawnCacheEvict切面的流程,代码如下:
@Around(value = "dawnCacheEvictsPointCut()")
public Object aroundDawnCacheEvictsPointCut(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Class<?> target = proceedingJoinPoint.getTarget().getClass();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
DawnCacheEvicts dawnCacheEvicts = AnnotationUtils.getAnnotation(method, DawnCacheEvicts.class);
Assert.notNull(dawnCacheEvicts, "never null");
log.debug("注解式拦截" + dawnCacheEvicts.toString());
Object result = proceedingJoinPoint.proceed();
DawnCacheEvict[] dawnCacheEvictArr = dawnCacheEvicts.dawnCacheEvicts();
String dawnCacheEvictsName = dawnCacheEvicts.name();
// 此处代码处理那些只有名称的“别名”型注解,如@DawnCacheEvicts("aaa")
if (dawnCacheEvictArr.length == 0) {
if (StringUtil.isEmpty(dawnCacheEvictsName)) {
log.warn("存在无名无配置的缓存清除注解:{}.{}", target.getName(), method.getName());
return result;
}
dawnCacheEvictArr = getDawnCacheEvictsForTarget(target, dawnCacheEvictsName);
}
if (dawnCacheEvictArr.length > 0) {
// 遍历,挨个处理
for (DawnCacheEvict dawnCacheEvict : dawnCacheEvictArr) {
handleDawnCacheEvict(proceedingJoinPoint, dawnCacheEvict);
}
}
return result;
}
通过这三个切面,实现了对缓存注解配置的处理,这样的处理过程使用者是无感知的,是不可见的。这也是AOP设计的优雅之处。
END(3)
下一篇:java 实现的数据查询缓存通用模型——缓存组件设计(4)
我还是忍不住会想你。
——平
版权声明
本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途
评论区#
还没有评论哦,期待您的评论!
引用发言