深入理解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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • JavaWeb开发之Spring+SpringMVC+MyBatis+SpringSecurity+EhCache+JCaptcha 完整Web基础框架

    简单介绍一下,本框架的基本功能点: Spring:整个框架的主体部分,这个自不用说. SpringMVC:MVC部分我还是比较喜欢Spring的. MyBatis:选型的时候选择这个ORM主要也是考虑其灵活性的问题,毕竟我也不知道,今后会遇到怎样的需求,用Hibernate一来是不太会用,二来,我还是比较喜欢直接写SQL来的简单一点. SpringSecurity:这个主要是安全框架,负责用户登录验证及整站权限分配的相关事项(权限分配真的很有用,这个我就不多说了). EhCache:一个非常流行

  • Spring Boot 简单使用EhCache缓存框架的方法

    我的环境是Gradle + Kotlin + Spring Boot,这里介绍EhCache缓存框架在Spring Boot上的简单应用. 在build.gradle文件添加依赖 compile("org.springframework.boot:spring-boot-starter-cache") compile("net.sf.ehcache:ehcache") 修改Application的配置,增加@EnableCaching配置 @MapperScan(&

  • 深入理解Spring Cache框架

    本文是缓存系列第三篇,前两篇分别介绍了 Guava 和 JetCache. 前两篇我们讲了 Guava 和 JetCache,它们都是缓存的具体实现,今天给大家分析一下 Spring 框架本身对这些缓存具体实现的支持和融合.使用 Spring Cache 将大大的减少我们的Spring项目中缓存使用的复杂度,提高代码可读性.本文将从以下几个方面来认识Spring Cache框架. 背景 SpringCache 产生的背景其实与Spring产生的背景有点类似.由于 Java EE 系统框架臃肿.低

  • Spring Cache框架应用介绍

    目录 介绍 常用注解 实际测试 介绍 Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能. Spring Cache提供了一层抽象,底层可以切换不同的cache实现.具体就是通过CacheManager接口来统一不同的缓存技术. CacheManager是Spring提供的各种缓存技术抽象接口. 针对不同的缓存技术需要实现不同的CacheManager: CacheManager 描述 EhCacheCacheManager 使用EhCache

  • 详解SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)

    引言 ​前两天在写一个实时数据处理的项目,项目要求是 1s 要处理掉 1k 的数据,这时候显然光靠查数据库是不行的,技术选型的时候老大跟我提了一下使用 Layering-Cache 这个开源项目来做缓存框架. ​之间问了一下身边的小伙伴,似乎对这块了解不多.一般也就用用 Redis 来缓存,应该是很少用多级缓存框架来专门性的管理缓存吧. ​趁着这个机会,我多了解了一些关于 SpringBoot 中缓存的相关技术,于是有了这篇文章! 在项目性能需求比较高时,就不能单单依赖数据库访问来获取数据了,必

  • spring boot+spring cache实现两级缓存(redis+caffeine)

    spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis.Caffeine.JCache.EhCache等等.但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存).在很多场景下,可以结合起来实现一.二级缓存的方式,能够很大程度提高应用的处理效率. 内容说明: 缓存.两级缓存 spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明 spring boot

  • Spring Cache的基本使用与实现原理详解

    Spring Cache 概念 从Spring 3.1版本开始,提供了一种透明的方式来为现有的Spring 应用添加cache,使用起来就像@Transaction一样.在应用层面与后端存储之间,提供了一层抽象,这层抽象目的在于封装各种可插拔的后端存储( Ehcache Guava Redis),最小化因为缓存给现有业务代码带来的侵入. Spring 的缓存技术还具备相当的灵活性.不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 cond

  • 详解在Spring Boot框架下使用WebSocket实现消息推送

    spring Boot的学习持续进行中.前面两篇博客我们介绍了如何使用Spring Boot容器搭建Web项目以及怎样为我们的Project添加HTTPS的支持,在这两篇文章的基础上,我们今天来看看如何在Spring Boot中使用WebSocket. 什么是WebSocket WebSocket为浏览器和服务器之间提供了双工异步通信功能,也就是说我们可以利用浏览器给服务器发送消息,服务器也可以给浏览器发送消息,目前主流浏览器的主流版本对WebSocket的支持都算是比较好的,但是在实际开发中使

  • 理解Spring中的依赖注入和控制反转

    学习过Spring框架的人一定都会听过Spring的IoC(控制反转) .DI(依赖注入)这两个概念,对于初学Spring的人来说,总觉得IoC .DI这两个概念是模糊不清的,是很难理解的,今天和大家分享网上的一些技术大牛们对Spring框架的IOC的理解以及谈谈我对Spring Ioc的理解. IoC是什么 Ioc-InversionofControl,即"控制反转",不是什么技术,而是一种设计思想.在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内

  • 简单理解Spring之IOC和AOP及代码示例

    Spring是一个开源框架,主要实现两件事,IOC(控制反转)和AOP(面向切面编程). IOC 控制反转,也可以称为依赖倒置. 所谓依赖,从程序的角度看,就是比如A要调用B的方法,那么A就依赖于B,反正A要用到B,则A依赖于B.所谓倒置,你必须理解如果不倒置,会怎么着,因为A必须要有B,才可以调用B,如果不倒置,意思就是A主动获取B的实例:Bb=newB(),这就是最简单的获取B实例的方法(当然还有各种设计模式可以帮助你去获得B的实例,比如工厂.Locator等等),然后你就可以调用b对象了.

  • Spring MVC框架配置方法详解

    本文实例为大家分享了Spring MVC框架配置方法,供大家参考,具体内容如下 1.概述 Spring MVC 作用:用来实现前端浏览器与后面程序的交互 Spring MVC 是基于Spring 的MVC框架,所谓MVC(model,controller,view) ,整个Spring MVC 作用就是,基于Spring 将model(数据)在controller(后台程序) ,view(前端浏览器)之间交互 至于Spring MVC优点缺点,了解不深 不作评价, 2.引用的jar包 既然是基于

  • Spring Cache整合Redis实现方法详解

    导入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>

随机推荐