深入理解Spring Cache框架
本文是緩存系列第三篇,前兩篇分別介紹了 Guava 和 JetCache。
前兩篇我們講了 Guava 和 JetCache,它們都是緩存的具體實現(xiàn),今天給大家分析一下 Spring 框架本身對這些緩存具體實現(xiàn)的支持和融合。使用 Spring Cache 將大大的減少我們的Spring項目中緩存使用的復(fù)雜度,提高代碼可讀性。本文將從以下幾個方面來認(rèn)識Spring Cache框架。
背景
SpringCache 產(chǎn)生的背景其實與Spring產(chǎn)生的背景有點類似。由于 Java EE 系統(tǒng)框架臃腫、低效,代碼可觀性低,對象創(chuàng)建和依賴關(guān)系復(fù)雜, Spring 框架出來了,目前基本上所有的Java后臺項目都離不開 Spring 或 SpringBoot (對 Spring 的進一步簡化)。現(xiàn)在項目面臨高并發(fā)的問題越來越多,各類緩存的應(yīng)用也增多,那么在通用的 Spring 框架上,就需要有一種更加便捷簡單的方式,來完成緩存的支持,就這樣 SpringCache就出現(xiàn)了。
不過首先我們需要明白的一點是,SpringCache 并非某一種 Cache 實現(xiàn)的技術(shù),SpringCache 是一種緩存實現(xiàn)的通用技術(shù),基于 Spring 提供的 Cache 框架,讓開發(fā)者更容易將自己的緩存實現(xiàn)高效便捷的嵌入到自己的項目中。當(dāng)然,SpringCache 也提供了本身的簡單實現(xiàn) NoOpCacheManager、ConcurrentMapCacheManager 等。通過 SpringCache,可以快速嵌入自己的Cache實現(xiàn)。
用法
源碼已分享至Github: https://github.com/zhuzhenke/common-caches
注意點:
1、開啟 EnableCaching 注解,默認(rèn)沒有開啟 Cache。
2、配置 CacheManager。
@Bean
@Qualifier("concurrentMapCacheManager")
@Primary
ConcurrentMapCacheManager concurrentMapCacheManager() {
return new ConcurrentMapCacheManager();
}
這里使用了 @Primary 和 @Qualifier 注解,@Qualifier 注解是給這個 Bean 加一個名字,用于同一個接口 Bean 的多個實現(xiàn)時,指定當(dāng)前 Bean 的名字,也就意味著 CacheManager 可以配置多個,并且在不同的方法場景下使用。@Primary 注解是當(dāng)接口 Bean 有多個時,優(yōu)先注入當(dāng)前 Bean 。
現(xiàn)在拿 CategoryService 實現(xiàn)來分析。
public class CategoryService {
@Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN,
key = "#category.getCategoryCacheKey()",
beforeInvocation = true)})
public int add(Category category) {
System.out.println("模擬進行數(shù)據(jù)庫交互操作......");
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("模擬進行數(shù)據(jù)庫交互操作......");
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("模擬進行數(shù)據(jù)庫交互操作......");
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("模擬進行數(shù)據(jù)庫交互操作......");
Category result = new Category();
result.setCateId(category.getCateId());
result.setCateName(category.getCateId() + "CateName");
result.setParentId(category.getCateId() - 10);
return result;
}
}
CategoryService 通過對 category 對象的數(shù)據(jù)庫增刪改查,模擬緩存失效和緩存增加的結(jié)果。使用非常簡便,把注解加在方法上,則可以達到緩存的生效和失效方案。
深入源碼
源碼分析我們分為幾個方面一步一步解釋其中的實現(xiàn)原理和實現(xiàn)細節(jié)。源碼基于 Spring 4.3.7.RELEASE 分析。
發(fā)現(xiàn)
SpringCache 在方法上使用注解發(fā)揮緩存的作用,緩存的發(fā)現(xiàn)是基于 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個注解后,會將注解中的參數(shù)解析出來,作為后續(xù)注解生效的一個依據(jù)。這里舉例說一下 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 注解是用于緩存失效。這里代碼會根據(jù) CacheEvict 的配置生產(chǎn)一個 CacheEvictOperation 的類,注解上的 name、key、cacheManager 和 beforeInvocation 等都會傳遞進來。
另外需要將一下 Caching 注解,這個注解通過 parseCachingAnnotation 方法解析參數(shù),會拆分成 Cacheable、CacheEvict、CachePut 注解,也就對應(yīng)我們緩存中的增加、失效和更新操作。
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 放在當(dāng)前 Map<Object, Collection<CacheOperation>> attributeCache = new ConcurrentHashMap<Object, Collection<CacheOperation>>(1024); 屬性上,為后續(xù)攔截方法時處理緩存做好數(shù)據(jù)的準(zhǔn)備。
注解產(chǎn)生作用
當(dāng)訪問 categoryService.get(category) 方法時,會走到 CglibAopProxy.intercept() 方法,這也說明緩存注解是基于動態(tài)代理實現(xiàn),通過方法的攔截來動態(tài)設(shè)置或失效緩存。方法中會通過 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 來拿到當(dāng)前調(diào)用方法的 Interceptor 鏈。往下走會調(diào)用 CacheInterceptor 的 invoke 方法,最終調(diào)用 execute 方法,我們重點分析這個方法的實現(xiàn)。
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 參數(shù)是 true,說明是方法執(zhí)行前進行的操作,這里是取出 CacheEvictOperation,operation.isBeforeInvocation(),調(diào)用下面方法:
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 中有個參數(shù) cacheWide,如果使用這個參數(shù)并設(shè)置為true,則在緩存失效時,會調(diào)用 clear 方法進行全部緩存的清理,否則只對當(dāng)前 key 進行 evict 操作。本文中,doEvict() 最終會調(diào)用到 ConcurrentMapCache的evict(Object key) 方法,將 key 緩存失效。
回到 execute 方法,走到 Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); 這一步,這里會根據(jù)當(dāng)前方法是否有 CacheableOperation 注解,進行緩存的查詢,如果沒有命中緩存,則會調(diào)用方法攔截器 CacheInterceptor 的 proceed 方法,進行原方法的調(diào)用,得到緩存 key 對應(yīng)的 value,然后通過 cachePutRequest.apply(cacheValue) 設(shè)置緩存。
public void apply(Object result) {
if (this.context.canPutToCache(result)) {
for (Cache cache : this.context.getCaches()) {
doPut(cache, this.key, result);
}
}
}
doPut() 方法最終對調(diào)用到 ConcurrentMapCache 的 put 方法,完成緩存的設(shè)置工作。
最后 execute 方法還有最后一步 processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); 處理針對執(zhí)行方法后緩存失效的注解策略。
優(yōu)缺點
優(yōu)點
方便快捷高效,可直接嵌入多個現(xiàn)有的 cache 實現(xiàn),簡寫了很多代碼,可觀性非常強。
缺點
- 內(nèi)部調(diào)用,非 public 方法上使用注解,會導(dǎo)致緩存無效。由于 SpringCache 是基于 Spring AOP 的動態(tài)代理實現(xiàn),由于代理本身的問題,當(dāng)同一個類中調(diào)用另一個方法,會導(dǎo)致另一個方法的緩存不能使用,這個在編碼上需要注意,避免在同一個類中這樣調(diào)用。如果非要這樣做,可以通過再次代理調(diào)用,如 ((Category)AopContext.currentProxy()).get(category) 這樣避免緩存無效。
- 不能支持多級緩存設(shè)置,如默認(rèn)到本地緩存取數(shù)據(jù),本地緩存沒有則去遠端緩存取數(shù)據(jù),然后遠程緩存取回來數(shù)據(jù)再存到本地緩存。
擴展知識點
- 動態(tài)代理:JDK、CGLIB代理。
- SpringAOP、方法攔截器。
Demo
https://github.com/zhuzhenke/common-caches
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java UrlRewriter偽靜態(tài)技術(shù)運用深入分析
通常我們?yōu)榱烁玫木徑夥?wù)器壓力,和增強搜索引擎的友好面,都將文章內(nèi)容生成靜態(tài)頁面,這就產(chǎn)生了偽靜態(tài)技術(shù),也就是我們常說的Url Rewriter重寫技術(shù)2012-12-12
Springboot整合Java?DL4J實現(xiàn)交通標(biāo)志識別系統(tǒng)全過程
在自動駕駛系統(tǒng)中,交通標(biāo)志識別是實現(xiàn)車輛智能化的關(guān)鍵技術(shù)之一,本文介紹了利用SpringBoot和JavaDeeplearning4j構(gòu)建交通標(biāo)志識別系統(tǒng)的方法,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-10-10
Spring項目使用Maven和BCrypt實現(xiàn)修改密碼功能方式
在數(shù)字時代,信息安全尤其是密碼安全至關(guān)重要,本文通過實例探討如何在Spring項目中利用Maven和BCrypt實現(xiàn)一個安全的密碼修改功能,我們將從環(huán)境搭建到編寫核心業(yè)務(wù)邏輯,再到完成功能測試,確保每一步都遵循最佳安全實踐,通過本文,你將了解到密碼安全的重要性2024-10-10
MyBatis實現(xiàn)插入大量數(shù)據(jù)方法詳解
最近在公司項目開發(fā)中遇到批量數(shù)據(jù)插入或者更新,下面這篇文章主要給大家介紹了關(guān)于MyBatis實現(xiàn)批量插入的相關(guān)資料,需要的朋友可以參考下2022-11-11

