一次排查@CacheEvict注解失效的經歷及解決
排查@CacheEvict注解失效
我簡單看了一下《Spring實戰(zhàn)》中的demo,然后就應用到業(yè)務代碼中了,本以為如此簡單的事情,竟然在代碼提交后的1個周,被同事發(fā)現(xiàn)。selectByTaskId()方法查出來的數(shù)據(jù)總是過時的。
代碼如下:
@Cacheable("taskParamsCache")
List<TaskParams> selectByTaskId(Long taskId);
// ...
// ...
@CacheEvict("taskParamsCache")
int deleteByTaskId(Long taskId);
想要的效果是當程序調用selectByTaskId()方法時,把結果緩存下來,然后在調用deleteByTaskId()方法時,將緩存清空。
經過數(shù)據(jù)庫數(shù)據(jù)對比之后,把問題排查的方向定位在@CacheEvict注解失效了。
下面是我通過源碼跟蹤排查問題的過程
在deleteByTaskId()方法的調用出打斷點,跟進代碼到spring生成的代理層。
@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// Check whether we only have one InvokerInterceptor: that is,
// no real advice, but just reflective invocation of the target.
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// We can skip creating a MethodInvocation: just invoke the target directly.
// Note that the final invoker must be an InvokerInterceptor, so we know
// it does nothing but a reflective operation on the target, and no hot
// swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// We need to create a method invocation...
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
通過getInterceptorsAndDynamicInterceptionAdvice獲取到當前方法的攔截器,里面包含了CacheIneterceptor,說明注解被spring檢測到了。

進入CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed()方法內部
org.springframework.aop.framework.ReflectiveMethodInvocation#proceed
@Override
@Nullable
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)方法取第一個攔截器,正是我們要關注的CacheIneterceptor,然后調用((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)方法,繼續(xù)跟進
org.springframework.cache.interceptor.CacheInterceptor#invoke
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
}
catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
try {
return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
進入execute方法
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class<?> targetClass = getTargetClass(target);
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
return execute(invoker, method,
new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
}
return invoker.invoke();
}
cacheOperationSource記錄系統(tǒng)中所有使用了緩存的方法,cacheOperationSource.getCacheOperations(method, targetClass)能獲取deleteByTaskId()方法緩存元數(shù)據(jù),然后執(zhí)行execute()方法
@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
這里大致過程是:
先執(zhí)行beforInvokeEvict ---- 執(zhí)行數(shù)據(jù)庫delete操作 --- 執(zhí)行CachePut操作 ---- 執(zhí)行afterInvokeEvict
我們的注解是方法調用后再使緩存失效,直接所以有效的操作應在倒數(shù)第2行
private void performCacheEvict(
CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {
Object key = null;
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
doClear(cache);
}
else {
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
doEvict(cache, key);
}
}
}
這里通過context.getCaches()獲取到name為taskParamsCache的緩存

然后generateKey生成key,注意這里,發(fā)現(xiàn)生成的key是com.xxx.xxx.atomic.impl.xxxxdeleteByTaskId982,但是緩存中的key卻是com.xxx.xxx.atomic.impl.xxxxselectByTaskId982,下面調用的doEvict(cache, key)方法不再跟進了,就是從cache中移除key對應值。明顯這里key對應不上的,這也是導致@CacheEvict沒有生效的原因。
小結一下
我還是太大意了,當時看了注解@CacheEvict的對key的注釋:

大意就是如果沒有指定key,那就會使用方法所有參數(shù)生成一個key,明顯com.xxx.xxx.atomic.impl.xxxxselectByTaskId982是方法名 + 參數(shù),可是你沒說把方法名還加上了啊,說好的只用參數(shù)呢,哈哈,這個bug是我使用不當引出的,很多人不會犯這種低級錯誤。
解決辦法就是使用SpEL明確定義key
@Cacheable(value = "taskParamsCache", key = "#taskId") List<TaskParams> selectByTaskId(Long taskId); // ... // ... @CacheEvict(value = "taskParamsCache", key = "#taskId") int deleteByTaskId(Long taskId);
說說spring全家桶中@CacheEvict無效情況
@CacheEvict(value =“test”, allEntries = true)
1、使用@CacheEvict注解的方法必須是controller層直接調用,service里間接調用不生效。
2、原因是因為key值跟你查詢方法的key值不統(tǒng)一,所以導致緩存并沒有清除
3、把@CacheEvict的方法和@Cache的方法放到一個java文件中寫,他倆在兩個java文件的話,會導致@CacheEvict失效。
4、返回值必須設置為void
It is important to note that void methods can be used with @CacheEvict
5、@CacheEvict必須作用在走代理的方法上
在使用Spring @CacheEvict注解的時候,要注意,如果類A的方法f1()被標注了 @CacheEvict注解,那么當類A的其他方法,例如:f2(),去直接調用f1()的時候, @CacheEvict是不起作用的,原因是 @CacheEvict是基于Spring AOP代理類,f2()屬于內部方法,直接調用f1()時,是不走代理的。
舉個例子
不生效:
@Override
public void saveEntity(Menu menu) {
try {
mapper.insert(menu);
//Cacheable 不生效
this.test();
}catch(Exception e){
e.printStackTrace();
}
}
@CacheEvict(value = "test" , allEntries = true)
public void test() {
}
正確使用:
@Override
@CacheEvict(value = "test" , allEntries = true)
public void saveEntity(Menu menu) {
try {
mapper.insert(menu);
}catch(Exception e){
e.printStackTrace();
}
}
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
SpringSecurity自定義資源攔截規(guī)則及登錄界面跳轉問題
這篇文章主要介紹了SpringSecurity自定義資源攔截規(guī)則及登錄界面跳轉問題,我們想要自定義認證邏輯,就需要創(chuàng)建一些原來不存在的bean,這個時候就可以使@ConditionalOnMissingBean注解,本文給大家介紹的非常詳細,需要的朋友參考下吧2023-12-12
將RestTemplate的編碼格式改為UTF-8,防止亂碼問題
這篇文章主要介紹了將RestTemplate的編碼格式改為UTF-8,防止亂碼問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
Java線程之鎖對象Lock-同步問題更完美的處理方式代碼實例
這篇文章主要介紹了Java線程之鎖對象Lock-同步問題更完美的處理方式代碼實例,還是挺不錯的,這里分享給大家,需要的朋友可以參考。2017-11-11

