@CacheEvict注解失效的经历及解决方法是什么,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。
排查@CacheEvict注解失效
我简单看了一下《Spring实战》中的demo,然后就应用到业务代码中了,本以为如此简单的事情,竟然在代码提交后的1个周,被同事发现。selectByTaskId()方法查出来的数据总是过时的。
代码如下:
@Cacheable("taskParamsCache")List<TaskParams> selectByTaskId(Long taskId);// ...// ...@CacheEvict("taskParamsCache")int deleteByTaskId(Long taskId);
想要的效果是当程序调用selectByTaskId()方法时,把结果缓存下来,然后在调用deleteByTaskId()方法时,将缓存清空。
经过数据库数据对比之后,把问题排查的方向定位在@CacheEvict注解失效了。
下面是我通过源码跟踪排查问题的过程
在deleteByTaskId()方法的调用出打断点,跟进代码到spring生成的代理层。
@Override@Nullablepublic 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@Nullablepublic 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)方法,继续跟进
org.springframework.cache.interceptor.CacheInterceptor#invoke
@Override@Nullablepublic 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记录系统中所有使用了缓存的方法,cacheOperationSource.getCacheOperations(method, targetClass)能获取deleteByTaskId()方法缓存元数据,然后执行execute()方法
@Nullableprivate Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {// Special handling of synchronized invocationif (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 methodreturn invokeOperation(invoker);}}// Process any early evictionsprocessCacheEvicts(contexts.get(CacheEvictOperation.class), true,CacheOperationExpressionEvaluator.NO_RESULT);// Check if we have a cached item matching the conditionsCache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));// Collect puts from any @Cacheable miss, if no cached item is foundList<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 hitcacheValue = cacheHit.get();returnValue = wrapCacheValue(method, cacheValue);}else {// Invoke the method if we don't have a cache hitreturnValue = invokeOperation(invoker);cacheValue = unwrapReturnValue(returnValue);}// Collect any explicit @CachePutscollectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);// Process any collected put requests, either from @CachePut or a @Cacheable missfor (CachePutRequest cachePutRequest : cachePutRequests) {cachePutRequest.apply(cacheValue);}// Process any late evictionsprocessCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);return returnValue;}
这里大致过程是:
先执行beforInvokeEvict ---- 执行数据库delete操作 --- 执行CachePut操作 ---- 执行afterInvokeEvict
我们的注解是方法调用后再使缓存失效,直接所以有效的操作应在倒数第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,注意这里,发现生成的key是com.xxx.xxx.atomic.impl.xxxxdeleteByTaskId982,但是缓存中的key却是com.xxx.xxx.atomic.impl.xxxxselectByTaskId982,下面调用的doEvict(cache, key)方法不再跟进了,就是从cache中移除key对应值。明显这里key对应不上的,这也是导致@CacheEvict没有生效的原因。
小结一下
我还是太大意了,当时看了注解@CacheEvict的对key的注释:
大意就是如果没有指定key,那就会使用方法所有参数生成一个key,明显com.xxx.xxx.atomic.impl.xxxxselectByTaskId982是方法名 + 参数,可是你没说把方法名还加上了啊,说好的只用参数呢,哈哈,这个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)
使用@CacheEvict注解的方法必须是controller层直接调用,service里间接调用不生效。
原因是因为key值跟你查询方法的key值不统一,所以导致缓存并没有清除
把@CacheEvict的方法和@Cache的方法放到一个java文件中写,他俩在两个java文件的话,会导致@CacheEvict失效。
返回值必须设置为void
@CacheEvict annotation
It is important to note that void methods can be used with @CacheEvict
@CacheEvict必须作用在走代理的方法上
在使用Spring @CacheEvict注解的时候,要注意,如果类A的方法f1()被标注了 @CacheEvict注解,那么当类A的其他方法,例如:f2(),去直接调用f1()的时候, @CacheEvict是不起作用的,原因是 @CacheEvict是基于Spring AOP代理类,f2()属于内部方法,直接调用f1()时,是不走代理的。
举个例子
不生效:
@Overridepublic 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(); }}
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注编程网行业资讯频道,感谢您对编程网的支持。