Spring Cache框架
本文是快取系列第三篇,前兩篇分別介紹了 Guava 和 JetCache。
前兩篇我們講了 Guava 和 JetCache,它們都是快取的具體實現,今天給大家分析一下 Spring 框架本身對這些快取具體實現的支援和融合。使用 Spring Cache 將大大的減少我們的Spring專案中快取使用的複雜度,提高程式碼可讀性。本文將從以下幾個方面來認識Spring Cache框架。
背景
SpringCache 產生的背景其實與Spring產生的背景有點類似。由於 Java EE 系統框架臃腫、低效,程式碼可觀性低,物件建立和依賴關係複雜, Spring 框架出來了,目前基本上所有的Java後臺專案都離不開 Spring 或 SpringBoot (對 Spring 的進一步簡化)。現在專案面臨高併發的問題越來越多,各類快取的應用也增多,那麼在通用的 Spring 框架上,就需要有一種更加便捷簡單的方式,來完成快取的支援,就這樣 SpringCache就出現了。
不過首先我們需要明白的一點是,SpringCache 並非某一種 Cache 實現的技術,SpringCache 是一種快取實現的通用技術,基於 Spring 提供的 Cache 框架,讓開發者更容易將自己的快取實現高效便捷的嵌入到自己的專案中。當然,SpringCache 也提供了本身的簡單實現 NoOpCacheManager、ConcurrentMapCacheManager 等。通過 SpringCache,可以快速嵌入自己的Cache實現。
用法
原始碼已分享至Github:ofollow,noindex" target="_blank">https://github.com/zhuzhenke/common-caches
注意點:
- 開啟 EnableCaching 註解,預設沒有開啟 Cache。
- 配置 CacheManager。
@Bean @Qualifier("concurrentMapCacheManager") @Primary ConcurrentMapCacheManager concurrentMapCacheManager() { return new ConcurrentMapCacheManager(); }
這裡使用了 @Primary 和 @Qualifier 註解,@Qualifier 註解是給這個 Bean 加一個名字,用於同一個介面 Bean 的多個實現時,指定當前 Bean 的名字,也就意味著 CacheManager 可以配置多個,並且在不同的方法場景下使用。@Primary 註解是當介面 Bean 有多個時,優先注入當前 Bean 。
現在拿 CategoryService 實現來分析。
public class CategoryService { @Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN, key = "#category.getCategoryCacheKey()", beforeInvocation = true)}) public int add(Category category) { System.out.println("模擬進行資料庫互動操作......"); System.out.println("Cache became invalid,value:" + CategoryCacheConstants.CATEGORY_DOMAIN + ",key:" + category.getCategoryCacheKey()); return 1; } @Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN, key = "#category.getCategoryCacheKey()", beforeInvocation = true)}) public int delete(Category category) { System.out.println("模擬進行資料庫互動操作......"); System.out.println("Cache became invalid,value:" + CategoryCacheConstants.CATEGORY_DOMAIN + ",key:" + category.getCategoryCacheKey()); return 0; } @Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN, key = "#category.getCategoryCacheKey()")}) public int update(Category category) { System.out.println("模擬進行資料庫互動操作......"); System.out.println("Cache updated,value:" + CategoryCacheConstants.CATEGORY_DOMAIN + ",key:" + category.getCategoryCacheKey() + ",category:" + category); return 1; } @Cacheable(value = CategoryCacheConstants.CATEGORY_DOMAIN, key = "#category.getCategoryCacheKey()") public Category get(Category category) { System.out.println("模擬進行資料庫互動操作......"); Category result = new Category(); result.setCateId(category.getCateId()); result.setCateName(category.getCateId() + "CateName"); result.setParentId(category.getCateId() - 10); return result; } }
CategoryService 通過對 category 物件的資料庫增刪改查,模擬快取失效和快取增加的結果。使用非常簡便,把註解加在方法上,則可以達到快取的生效和失效方案。
深入原始碼
原始碼分析我們分為幾個方面一步一步解釋其中的實現原理和實現細節。原始碼基於 Spring 4.3.7.RELEASE 分析。
發現
SpringCache 在方法上使用註解發揮快取的作用,快取的發現是基於 AOP 的 PointCut 和 MethodMatcher 通過在注入的 class 中找到每個方法上的註解,並解析出來。
首先看到 org.springframework.cache.annotation.SpringCacheAnnotationParser 類:
protected Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { Collection<CacheOperation> ops = null; Collection<Cacheable> cacheables = AnnotatedElementUtils.getAllMergedAnnotations(ae, Cacheable.class); if (!cacheables.isEmpty()) { ops = lazyInit(ops); for (Cacheable cacheable : cacheables) { ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable)); } } Collection<CacheEvict> evicts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CacheEvict.class); if (!evicts.isEmpty()) { ops = lazyInit(ops); for (CacheEvict evict : evicts) { ops.add(parseEvictAnnotation(ae, cachingConfig, evict)); } } Collection<CachePut> puts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CachePut.class); if (!puts.isEmpty()) { ops = lazyInit(ops); for (CachePut put : puts) { ops.add(parsePutAnnotation(ae, cachingConfig, put)); } } Collection<Caching> cachings = AnnotatedElementUtils.getAllMergedAnnotations(ae, Caching.class); if (!cachings.isEmpty()) { ops = lazyInit(ops); for (Caching caching : cachings) { Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching); if (cachingOps != null) { ops.addAll(cachingOps); } } } return ops; }
這個方法會解析 Cacheable、CacheEvict、CachePut 和 Caching 4個註解,找到方法上的這4個註解後,會將註解中的引數解析出來,作為後續註解生效的一個依據。這裡舉例說一下 CacheEvict 註解。
CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, CacheEvict cacheEvict) { CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder(); builder.setName(ae.toString()); builder.setCacheNames(cacheEvict.cacheNames()); builder.setCondition(cacheEvict.condition()); builder.setKey(cacheEvict.key()); builder.setKeyGenerator(cacheEvict.keyGenerator()); builder.setCacheManager(cacheEvict.cacheManager()); builder.setCacheResolver(cacheEvict.cacheResolver()); builder.setCacheWide(cacheEvict.allEntries()); builder.setBeforeInvocation(cacheEvict.beforeInvocation()); defaultConfig.applyDefault(builder); CacheEvictOperation op = builder.build(); validateCacheOperation(ae, op); return op; }
CacheEvict 註解是用於快取失效。這裡程式碼會根據 CacheEvict 的配置生產一個 CacheEvictOperation 的類,註解上的 name、key、cacheManager 和 beforeInvocation 等都會傳遞進來。
另外需要將一下 Caching 註解,這個註解通過 parseCachingAnnotation 方法解析引數,會拆分成 Cacheable、CacheEvict、CachePut 註解,也就對應我們快取中的增加、失效和更新操作。
Collection<CacheOperation> parseCachingAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Caching caching) { Collection<CacheOperation> ops = null; Cacheable[] cacheables = caching.cacheable(); if (!ObjectUtils.isEmpty(cacheables)) { ops = lazyInit(ops); for (Cacheable cacheable : cacheables) { ops.add(parseCacheableAnnotation(ae, defaultConfig, cacheable)); } } CacheEvict[] cacheEvicts = caching.evict(); if (!ObjectUtils.isEmpty(cacheEvicts)) { ops = lazyInit(ops); for (CacheEvict cacheEvict : cacheEvicts) { ops.add(parseEvictAnnotation(ae, defaultConfig, cacheEvict)); } } CachePut[] cachePuts = caching.put(); if (!ObjectUtils.isEmpty(cachePuts)) { ops = lazyInit(ops); for (CachePut cachePut : cachePuts) { ops.add(parsePutAnnotation(ae, defaultConfig, cachePut)); } } return ops; }
然後回到 AbstractFallbackCacheOperationSource 類:
public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) { if (method.getDeclaringClass() == Object.class) { return null; } Object cacheKey = getCacheKey(method, targetClass); Collection<CacheOperation> cached = this.attributeCache.get(cacheKey); if (cached != null) { return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); } else { Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass); if (cacheOps != null) { if (logger.isDebugEnabled()) { logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); } this.attributeCache.put(cacheKey, cacheOps); } else { this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); } return cacheOps; } }
這裡會將解析出來的 CacheOperation 放在當前 Map<Object, Collection<CacheOperation>> attributeCache = new ConcurrentHashMap<Object, Collection<CacheOperation>>(1024); 屬性上,為後續攔截方法時處理快取做好資料的準備。
註解產生作用
當訪問 categoryService.get(category) 方法時,會走到 CglibAopProxy.intercept() 方法,這也說明快取註解是基於動態代理實現,通過方法的攔截來動態設定或失效快取。方法中會通過 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 來拿到當前呼叫方法的 Interceptor 鏈。往下走會呼叫 CacheInterceptor 的 invoke 方法,最終呼叫 execute 方法,我們重點分析這個方法的實現。
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, new Callable<Object>() { @Override public Object call() throws Exception { return 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<CachePutRequest>(); 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; }
我們的方法沒有使用同步,走到 processCacheEvicts 方法。
private void processCacheEvicts(Collection<CacheOperationContext> contexts, boolean beforeInvocation, Object result) { for (CacheOperationContext context : contexts) { CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation; if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) { performCacheEvict(context, operation, result); } } }
注意這個方法傳入的 beforeInvocation 引數是 true,說明是方法執行前進行的操作,這裡是取出 CacheEvictOperation,operation.isBeforeInvocation(),呼叫下面方法:
private void performCacheEvict(CacheOperationContext context, CacheEvictOperation operation, Object result) { Object key = null; for (Cache cache : context.getCaches()) { if (operation.isCacheWide()) { logInvalidating(context, operation, null); doClear(cache); } else { if (key == null) { key = context.generateKey(result); } logInvalidating(context, operation, key); doEvict(cache, key); } } }
這裡需要注意了,operation 中有個引數 cacheWide,如果使用這個引數並設定為true,則在快取失效時,會呼叫 clear 方法進行全部快取的清理,否則只對當前 key 進行 evict 操作。本文中,doEvict() 最終會呼叫到 ConcurrentMapCache的evict(Object key) 方法,將 key 快取失效。
回到 execute 方法,走到 Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); 這一步,這裡會根據當前方法是否有 CacheableOperation 註解,進行快取的查詢,如果沒有命中快取,則會呼叫方法攔截器 CacheInterceptor 的 proceed 方法,進行原方法的呼叫,得到快取 key 對應的 value,然後通過 cachePutRequest.apply(cacheValue) 設定快取。
public void apply(Object result) { if (this.context.canPutToCache(result)) { for (Cache cache : this.context.getCaches()) { doPut(cache, this.key, result); } } }
doPut() 方法最終對呼叫到 ConcurrentMapCache 的 put 方法,完成快取的設定工作。
最後 execute 方法還有最後一步 processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); 處理針對執行方法後快取失效的註解策略。
優缺點
- 方便快捷高效,可直接嵌入多個現有的 cache 實現,簡寫了很多程式碼,可觀性非常強。
缺點
- 內部呼叫,非 public 方法上使用註解,會導致快取無效。由於 SpringCache 是基於 Spring AOP 的動態代理實現,由於代理本身的問題,當同一個類中呼叫另一個方法,會導致另一個方法的快取不能使用,這個在編碼上需要注意,避免在同一個類中這樣呼叫。如果非要這樣做,可以通過再次代理呼叫,如 ((Category)AopContext.currentProxy()).get(category) 這樣避免快取無效。
- 不能支援多級快取設定,如預設到本地快取取資料,本地快取沒有則去遠端快取取資料,然後遠端快取取回來資料再存到本地快取。
擴充套件知識點
- 動態代理:JDK、CGLIB代理。
- SpringAOP、方法攔截器。