基於spring @Cacheable 註解的spel表達式解析執行邏輯
日常使用中spring的 @Cacheable 大傢一定不陌生,基於aop機制的緩存實現,並且可以選擇cacheManager具體提供緩存的中間件或者進程內緩存,類似於 @Transactional 的transactionManager ,都是提供瞭一種多態的實現,抽象出上層接口,實現則供客戶端選擇,或許這就是架構吧,抽象的設計,使用interface對外暴露可擴展實現的機制,使用abstract 整合類似實現。
那麼我們就看看 @Cacheable提供的一種方便的機制,spel表達式取方法 參數的邏輯,大傢都寫過註解,但是註解邏輯需要的參數可以使用spel動態取值是不是好爽~
直接進入主題 跟隨spring的調用鏈
直接看 @Cacheable 註解就可以瞭
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Cacheable { // spring的別名機制,這裡不討論,和cacheNames作用一致 @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; // 今天的主角,就從他入手 String key() default ""; // 拼接key的 抽象出來的接口 String keyGenerator() default ""; // 真正做緩存這件事的人,redis,caffine,還是其他的都可以,至於內存還是進程上層抽象的邏輯不關心,如果你使用caffine //就需要自己考慮 多服務實例的一致性瞭 String cacheManager() default ""; String cacheResolver() default ""; // 是否可以執行緩存的條件 也是 spel 如果返回結果true 則進行緩存 String condition() default ""; // 如果spel 返回true 則不進行緩存 String unless() default ""; // 是否異步執行 boolean sync() default false; }
接下來看 key獲取是在哪裡
SpringCacheAnnotationParser#parseCacheableAnnotation 解析註解,還好就一個地方
沒有任何邏輯就是一個組裝
繼續跟蹤上述方法 SpringCacheAnnotationParser#parseCacheAnnotations 走到這裡,
@Nullable private Collection<CacheOperation> parseCacheAnnotations( DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) { Collection<? extends Annotation> anns = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) : AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS)); if (anns.isEmpty()) { return null; } final Collection<CacheOperation> ops = new ArrayList<>(1); anns.stream().filter(ann -> ann instanceof Cacheable).forEach( ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann))); anns.stream().filter(ann -> ann instanceof CacheEvict).forEach( ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann))); anns.stream().filter(ann -> ann instanceof CachePut).forEach( ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann))); anns.stream().filter(ann -> ann instanceof Caching).forEach( ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops)); return ops; }
也沒有太多邏輯,將當前攔截到的方法可能存在的多個 SpringCache的註解解析為集合返回,那就是支持多個SpringCache註解同時放到一個方法嘍。
@Override @Nullable public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) { // 到上邊發現這裡入參是一個類,那麼可以推斷這裡調用是啟動或者類加載時進行註解解析,然後緩存註解的寫死的參數返回 DefaultCacheConfig defaultConfig = new DefaultCacheConfig(type); return parseCacheAnnotations(defaultConfig, type); } //------------還有一個方法是對方法的解析也是對註解的解析返回------------------ @Override @Nullable public Collection<CacheOperation> parseCacheAnnotations(Method method) { DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass()); return parseCacheAnnotations(defaultConfig, method); }
再上邊 AnnotationCacheOperationSource#findCacheOperations ,兩個重載方法
@Override @Nullable protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) { return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz)); } @Override @Nullable protected Collection<CacheOperation> findCacheOperations(Method method) { return determineCacheOperations(parser -> parser.parseCacheAnnotations(method)); }
AbstractFallbackCacheOperationSource#computeCacheOperations 這裡有點看不懂暫時不細做追溯,目的就是spel
AbstractFallbackCacheOperationSource#getCacheOperations 還是處理解析註解返回
調用getCacheOperations方法的地方
如上圖直接查看第一個調用
CacheAspectSupport#execute 查看這個execute調用方是CacheInterceptor#invoke 實現的MethodInterceptor接口,那不用看其他的瞭,這裡就是執行方法攔截的地方,在這裡會找到spel的動態解析噢
順便看一下攔截方法中的執行邏輯
瞭解一下@Cacheable的攔截順序
@Override @Nullable public Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); // 這是個一個 函數式接口作為回調,這裡並沒有執行,先執行下面execute方法 即CacheAspectSupport#execute 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方法
@Nullable 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(); }
再看 重載方法execute
@Nullable private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { // 註解上的是否異步的字段這裡決定是否異步執行 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) { // Directly propagate ThrowableWrapper from the invoker, // or potentially also an IllegalArgumentException etc. ReflectionUtils.rethrowRuntimeException(ex.getCause()); } } else { // No caching required, only call the underlying method return invokeOperation(invoker); } } // -------------同步執行緩存邏輯-------------- // --------------------下面各種註解分別執行,可以看出來springCache註解之間的順序 緩存刪除(目標方法invoke前)並執行、緩存增 //加(猜測是先命中一次緩存,如果沒有命中先存入空數據的緩存,提前占住緩存數據,盡量減少並發緩存帶來的緩存沖洗問題)、 //緩存增加(帶有數據的)、上述兩個緩存增加的真正執行 、緩存刪除(目標方法invoke 後)並執行 //當然這個 是 invoke前執行 或者後執行 是取決於@CacheEvict 中的 beforeInvocation 配置,默認false在後面執行如果前面執行unless就拿不到結果值瞭 // 那麼spring cache 不是 延時雙刪噢,高並發可能存在數據過期數據重新灌入 // 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); } // 方法入參解析 用於 key condition Object cacheValue; // 方法結果 解析 用於 unless Object returnValue; if (cacheHit != null && !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; }
不詳細探究執行邏輯瞭,來看看生成key的邏輯,private 方法 generateKey
// 可以看出沒有生成key 會拋出異常,不允許null private Object generateKey(CacheOperationContext context, @Nullable Object result) { Object key = context.generateKey(result); if (key == null) { throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " + "using named params on classes without debug info?) " + context.metadata.operation); } if (logger.isTraceEnabled()) { logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation); } return key; } //------------------------繼續------------ /** * Compute the key for the given caching operation. */ @Nullable protected Object generateKey(@Nullable Object result) { if (StringUtils.hasText(this.metadata.operation.getKey())) { // 終於看到 spring核心包之一 org.springframework.expression 包裡的類瞭。。。T.T EvaluationContext evaluationContext = createEvaluationContext(result); return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); } return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); }
可以看到使用的 evaluator 是CacheOperationExpressionEvaluator類這個成員變量,類加載時便生成,裡面有生成待解析實例的方法,有解析 key condition unless 的三個方法及ConcurrentMap 成員變量緩存到內存中,將所有的Cache註解的 spel表達式緩存於此,默認 64的大小,主要方法如下
public EvaluationContext createEvaluationContext(Collection<? extends Cache> caches, Method method, Object[] args, Object target, Class<?> targetClass, Method targetMethod, @Nullable Object result, @Nullable BeanFactory beanFactory) { CacheExpressionRootObject rootObject = new CacheExpressionRootObject( caches, method, args, target, targetClass); CacheEvaluationContext evaluationContext = new CacheEvaluationContext( rootObject, targetMethod, args, getParameterNameDiscoverer()); if (result == RESULT_UNAVAILABLE) { evaluationContext.addUnavailableVariable(RESULT_VARIABLE); } else if (result != NO_RESULT) { evaluationContext.setVariable(RESULT_VARIABLE, result); } if (beanFactory != null) { evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); } return evaluationContext; } @Nullable public Object key(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { return getExpression(this.keyCache, methodKey, keyExpression).getValue(evalContext); } public boolean condition(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue( evalContext, Boolean.class))); } public boolean unless(String unlessExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { return (Boolean.TRUE.equals(getExpression(this.unlessCache, methodKey, unlessExpression).getValue( evalContext, Boolean.class))); }
然後就返回想要的key瞭。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Spring @Cacheable指定失效時間實例
- spring cache註解@Cacheable緩存穿透詳解
- 一次排查@CacheEvict註解失效的經歷及解決
- 關於Spring Cache 緩存攔截器( CacheInterceptor)
- Spring @Cacheable註解類內部調用失效的解決方案