1. 程式人生 > >快取篇(三)- Spring Cache框架

快取篇(三)- Spring Cache框架

前兩篇我們講了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:https://github.com/zhuzhenke/common-caches

注意點:

1、開啟EnableCaching註解,預設是沒有開啟Cache的

2、配置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和Caching4個註解,找到方法上的這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);處理針對執行方法後快取失效的註解策略。

 

優缺點

優點

1、方便快捷高效,可直接嵌入多個現有的cache實現,簡寫了很多程式碼,可觀性非常強

 

缺點

1、內部呼叫,非public方法上使用註解,會導致快取無效。由於SpringCache是基於Spring AOP的動態代理實現,由於代理本身的問題,當同一個類中呼叫另一個方法,會導致另一個方法的快取不能使用,這個在編碼上需要注意,避免在同一個類中這樣呼叫。如果非要這樣做,可以通過再次代理呼叫,如((Category)AopContext.currentProxy()).get(category)這樣避免快取無效

2、不能支援多級快取設定,如預設到本地快取取資料,本地快取沒有則去遠端快取取資料,然後遠端快取取回來資料再存到本地快取。

 

擴充套件知識點

1、動態代理:JDK、CGLIB代理

2、SpringAOP、方法攔截器

 

demo連結

https://github.com/zhuzhenke/common-caches

 

參考連結

Spring Cache:https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/index.html