深入理解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: 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 和 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、方法拦截器。
Demo
https://github.com/zhuzhenke/common-caches
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。