缓存篇(三)- Spring Cache框架

爱茂勋
2023-12-01

前两篇我们讲了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

 类似资料: