一次排查@CacheEvict注解失效的經(jīng)歷及解決
排查@CacheEvict注解失效
我簡單看了一下《Spring實(shí)戰(zhàn)》中的demo,然后就應(yīng)用到業(yè)務(wù)代碼中了,本以為如此簡單的事情,竟然在代碼提交后的1個(gè)周,被同事發(fā)現(xiàn)。selectByTaskId()方法查出來的數(shù)據(jù)總是過時(shí)的。
代碼如下:
@Cacheable("taskParamsCache") List<TaskParams> selectByTaskId(Long taskId); // ... // ... @CacheEvict("taskParamsCache") int deleteByTaskId(Long taskId);
想要的效果是當(dāng)程序調(diào)用selectByTaskId()方法時(shí),把結(jié)果緩存下來,然后在調(diào)用deleteByTaskId()方法時(shí),將緩存清空。
經(jīng)過數(shù)據(jù)庫數(shù)據(jù)對比之后,把問題排查的方向定位在@CacheEvict注解失效了。
下面是我通過源碼跟蹤排查問題的過程
在deleteByTaskId()方法的調(diào)用出打斷點(diǎn),跟進(jìn)代碼到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獲取到當(dāng)前方法的攔截器,里面包含了CacheIneterceptor,說明注解被spring檢測到了。
進(jìn)入CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed()方法內(nèi)部
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)方法取第一個(gè)攔截器,正是我們要關(guān)注的CacheIneterceptor,然后調(diào)用((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)方法,繼續(xù)跟進(jìn)
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(); } }
進(jìn)入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
我們的注解是方法調(diào)用后再使緩存失效,直接所以有效的操作應(yīng)在倒數(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,下面調(diào)用的doEvict(cache, key)方法不再跟進(jìn)了,就是從cache中移除key對應(yīng)值。明顯這里key對應(yīng)不上的,這也是導(dǎo)致@CacheEvict沒有生效的原因。
小結(jié)一下
我還是太大意了,當(dāng)時(shí)看了注解@CacheEvict的對key的注釋:
大意就是如果沒有指定key,那就會使用方法所有參數(shù)生成一個(gè)key,明顯com.xxx.xxx.atomic.impl.xxxxselectByTaskId982是方法名 + 參數(shù),可是你沒說把方法名還加上了啊,說好的只用參數(shù)呢,哈哈,這個(gè)bug是我使用不當(dāng)引出的,很多人不會犯這種低級錯(cuò)誤。
解決辦法就是使用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層直接調(diào)用,service里間接調(diào)用不生效。
2、原因是因?yàn)閗ey值跟你查詢方法的key值不統(tǒng)一,所以導(dǎo)致緩存并沒有清除
3、把@CacheEvict的方法和@Cache的方法放到一個(gè)java文件中寫,他倆在兩個(gè)java文件的話,會導(dǎo)致@CacheEvict失效。
4、返回值必須設(shè)置為void
It is important to note that void methods can be used with @CacheEvict
5、@CacheEvict必須作用在走代理的方法上
在使用Spring @CacheEvict注解的時(shí)候,要注意,如果類A的方法f1()被標(biāo)注了 @CacheEvict注解,那么當(dāng)類A的其他方法,例如:f2(),去直接調(diào)用f1()的時(shí)候, @CacheEvict是不起作用的,原因是 @CacheEvict是基于Spring AOP代理類,f2()屬于內(nèi)部方法,直接調(diào)用f1()時(shí),是不走代理的。
舉個(gè)例子
不生效:
@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(); } }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于StringUtils.isBlank()的使用及說明
這篇文章主要介紹了關(guān)于StringUtils.isBlank()的使用及說明,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05Java實(shí)現(xiàn)合并word文檔的示例代碼
在做項(xiàng)目中,經(jīng)常會遇到一種情況,需要將一個(gè)小word文檔的內(nèi)容插入到一個(gè)大word(主文檔)中。本文就為大家準(zhǔn)備了Java實(shí)現(xiàn)合并word文檔的方法,需要的可以參考一下2022-08-08SpringSecurity自定義資源攔截規(guī)則及登錄界面跳轉(zhuǎn)問題
這篇文章主要介紹了SpringSecurity自定義資源攔截規(guī)則及登錄界面跳轉(zhuǎn)問題,我們想要自定義認(rèn)證邏輯,就需要?jiǎng)?chuàng)建一些原來不存在的bean,這個(gè)時(shí)候就可以使@ConditionalOnMissingBean注解,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2023-12-12將RestTemplate的編碼格式改為UTF-8,防止亂碼問題
這篇文章主要介紹了將RestTemplate的編碼格式改為UTF-8,防止亂碼問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Java線程之鎖對象Lock-同步問題更完美的處理方式代碼實(shí)例
這篇文章主要介紹了Java線程之鎖對象Lock-同步問題更完美的處理方式代碼實(shí)例,還是挺不錯(cuò)的,這里分享給大家,需要的朋友可以參考。2017-11-11