Spring Cache在业务系统中最佳实践教程详解及实现原理

1.概述

接着之前总结的如何保证MySQL与Redis数据同步一致性一文中提到在业务代码中一般采用旁路缓存策略 方式实现同步,Spring Cache 就是 Spring Framework 基于该策略方式提供的一种缓存抽象,可以帮助开发者简化缓存的使用过程。它支持多种缓存实现,并通过注解的方式提供简洁的缓存操作。核心思想是通过缓存抽象层解耦具体的缓存实现,从而可以灵活地替换和配置不同的缓存方案 ,在 spring-context 包中定义了org.springframework.cache.Cacheorg.springframework.cache.CacheManager 两个顶层接口来统一不同的缓存技术。

这种抽象思想是非常值得学习的,比如平时我们在业务系统代码中通过 Kafka 来发送异步消息。对于这样一个功能的开发,我们要学会将其抽象成一组跟具体消息队列(Kafka)无关的异步消息接口。所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当我们要替换新的消息队列的时候,比如将 Kafka 替换成 RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现,同样地在文件存储、短信平台等服务开发中都需要使用这种封装抽象思想去应对变化,比如说文件存储从阿里云oss变为nas,短信从云片替换为腾讯云等等,这些在我们平时系统开发过程中已经司空见惯了。

Spring Cache 提供核心顶层封装接口:

  • Cache :表示具体的缓存,提供基本的缓存操作方法(如 getputevict 等)。类似于Spring框架中的BeanFactory

  • CacheManager:缓存管理器,负责创建和管理多个 Cache 实例。降低对各种缓存框架的耦合。它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Caffeine、Guava Cache、Redis等等。

2.基于声明式注解的Spring Cache使用案例

对于缓存声明,Spring的缓存抽象提供了一组Java注解:

  • @Cacheable: 触发缓存的填充。
  • @CacheEvict: 触发缓存驱逐。
  • @CachePut: 更新缓存而不干扰方法的执行。
  • @Caching: 将多个缓存操作重新分组,应用在一个方法上。
  • @CacheConfig: 分享一些常见的类级别的缓存相关设置。

2.1 代码上下文环境准备

这里我们使用缓存实现是Redis,所以引入如下依赖:

xml 复制代码
   <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
   <!-- 解决LocalDateTime字段缓存序列化 -->
  <dependency>
      <artifactId>jackson-datatype-jsr310</artifactId>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <version>2.15.3</version>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
  </dependency>

配置文件设置cache类型:指定为redis

java 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/db_test?&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
  redis:
    host: 10.10.0.10
    port: 6379
  application:
    name: base-demo111
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    locale: zh_CN
    time-zone: GMT+8
    default-property-inclusion: non_null
  cache:
    type: redis

添加配置类:

java 复制代码
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    /**
     * 配置文件的配置没有用上
     * 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")
     *                                public class CacheProperties
     * 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class)
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置key的序列化
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        // 设置value的序列化
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //将配置文件中所有的配置都生效
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

@EnableCaching:开启缓存功能,需要放到@Configuration 类中。即使声明了缓存注解,也不会自动触发它们的动作---就像Spring中的许多东西一样,这个功能必须要声明启用(这意味着如果你怀疑缓存是罪魁祸首,你可以只通过删除一个配置行而不是代码中的所有注解来禁用它)。要启用缓存注解,请将注解 @EnableCaching 添加到你的一个 @Configuration 类中

根据配置信息设置RedisCacheConfiguration,并指定自定义序列化方式,替代默认的JDK序列化机制,因为JDK序列化在Redis查看键值对不方便,像乱码一样,其次还可能出现兼容问题。

缓存的实体类信息:

java 复制代码
public class User {

    @TableId(type = IdType.AUTO)

    private Long id;
    private String userNo;
    private Integer gender;
    private String name;
    @JsonDeserialize(using = LocalDateDeserializer.class)
    @JsonSerialize(using = LocalDateSerializer.class)
    private LocalDate birthday;
    private String phone;
    private String email;
    private Integer isDelete;
    private String address;
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime createTime;
}

使用@JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class)解决LocalDateTime字段缓存序列化问题,关于LocalDateTime的使用请看:Spring Boot项目如何优雅丝滑地从Date过渡到LocalDateTime

接下来就可以使用相关注解进行缓存操作了

2.2 @Cacheable

@Cacheable 注解用于标记方法,其返回值会被缓存。每次调用该方法时,首先检查缓存是否存在,如果存在则返回缓存值,否则执行方法并将结果存入缓存,所以作用在void方法上是没有意义的,先来看看示例:

java 复制代码
    @Cacheable(cacheNames = "user")
    public User getUser(Long id) {
        User user = this.getById(id);
        log.info("走数据库查询了......");
        return user;
    }

单元测试代码:

java 复制代码
  @Test
    public void testGetUser() {
        User user = userService.getUser(8L);
        System.out.println(user);
    }

执行结果控制台输出如下:

2024-08-07 11:19:10.098 ShepherddeMacBook-Pro.local [common-demo] [] INFO com.zaxxer.hikari.HikariDataSource getConnection [main@55461] : HikariPool-1 - Starting...
2024-08-07 11:19:10.279 ShepherddeMacBook-Pro.local [common-demo] [] INFO com.zaxxer.hikari.HikariDataSource getConnection [main@55461] : HikariPool-1 - Start completed.
2024-08-07 11:19:10.293 ShepherddeMacBook-Pro.local [common-demo] [] DEBUG com.shepherd.basedemo.dao.UserDAO.selectById debug [main@55461] : ==>  Preparing: SELECT id,user_no,gender,name,birthday,phone,email,is_delete,address,create_time FROM tb_user WHERE id=?
2024-08-07 11:19:10.314 ShepherddeMacBook-Pro.local [common-demo] [] DEBUG com.shepherd.basedemo.dao.UserDAO.selectById debug [main@55461] : ==> Parameters: 8(Long)
2024-08-07 11:19:10.343 ShepherddeMacBook-Pro.local [common-demo] [] DEBUG com.shepherd.basedemo.dao.UserDAO.selectById debug [main@55461] : <==      Total: 1
2024-08-07 11:19:10.349 ShepherddeMacBook-Pro.local [common-demo] [] INFO com.shepherd.basedemo.service.UserService getUser [main@55461] : 走数据库查询了......
User(id=8, userNo=001, gender=0, name=张三, birthday=2024-08-07, phone=12234, email=null, isDelete=0, address=null, createTime=2024-07-03T16:09:12)

第一次执行走数据库查询了,并同步了缓存:

再次执行单元测试就走缓存了。接下来我们来看看的@Cacheable的定义及属性功能,好几个属性在@CacheEvict,@CachePut中也有,算是Spring Cache中的公共属性了

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {

	@AliasFor("cacheNames")
	String[] value() default {};

	@AliasFor("value")
	String[] cacheNames() default {};

	String key() default "";

	String keyGenerator() default "";

	String cacheManager() default "";

	String cacheResolver() default "";

	String condition() default "";

	String unless() default "";

	boolean sync() default false;

}

cacheNames :每个注解中都有自己的缓存名字。该名字的缓存与方法相关联,每次调用时,都会检查缓存以查看是否有对应cacheNames名字的数据,有数据就直接从缓存中读取返回。虽然在大多数情况下,只有一个缓存被声明,但注解允许指定多个名字,这样就可以使用多个缓存了。在这种情况下,每个缓存在调用方法之前都会被检查---如果至少有一个缓存被命中,相关的值会被返回。Spring Cache提供两个参数来指定缓存名:value、cacheNames,二者选其一即可,每一个需要缓存的数据都需要指定要放到哪个名字的缓存,缓存的分区,按照业务类型分 。

下面是用户信息需要缓存用户服务、订单服务两个分区的示例

java 复制代码
    @Cacheable(cacheNames = {"user", "order"})
    public User getUser(Long id) {
        User user = this.getById(id);
        log.info("走数据库查询了......");
        return user;
    }

同步缓存如图所示:

从图中看到cacheNames缓存名字就是分组、分区的作用,而我们缓存key是通过一定策略生成的,下面就来看看

keyGenerator :由于缓存本质上是键值存储 ,每一次对缓存方法的调用都需要转化为一个合适的键(key),以便缓存访问。缓存抽象默认提供了一个简单 SimpleKeyGenerator

java 复制代码
public class SimpleKeyGenerator implements KeyGenerator {

	@Override
	public Object generate(Object target, Method method, Object... params) {
		return generateKey(params);
	}

	/**
	 * Generate a key based on the specified parameters.
	 */
	public static Object generateKey(Object... params) {
		if (params.length == 0) {
			return SimpleKey.EMPTY;
		}
		if (params.length == 1) {
			Object param = params[0];
			if (param != null && !param.getClass().isArray()) {
				return param;
			}
		}
		return new SimpleKey(params);
	}

}
  • 如果没有给出参数,返回 SimpleKey.EMPTY
  • 如果只给出一个参数,则返回该实例。
  • 如果给了一个以上的参数,返回一个包含所有参数的 SimpleKey

很显然上面的示例的key:user::8就是通过SimpleKeyGenerator策略生成的

要提供一个不同的默认 key 生成器,你需要实现 org.springframework.cache.interceptor.KeyGenerator 接口。

key :缓存的key。当目标方法有多个参数,而其中只有一些适合缓存(其余的只用于方法逻辑)时,再使用KeyGenerator生成就不合适了,通过注解的 key 属性来自定义指定key,你可以使用 SpEL 来挑选感兴趣的参数(或它们的嵌套属性),执行操作,甚至调用任意的方法,而不需要编写任何代码或实现任何接口。如下所示:

java 复制代码
    @Cacheable(cacheNames = {"user", "order"}, key = "#userDTO.id")
    public User getUser(UserDTO userDTO) {
        Long id = userDTO.getId();
        User user = this.getById(id);
        log.info("走数据库查询了......");
        return user;
    }

keykeyGenerator 属性是相互排斥的,同时指定这两个参数的操作会产生一个异常。

可用的缓存 SpEL Evaluation Context

每个 SpEL 表达式都针对一个专门的 context 进行评估。除了内置参数外,框架还提供了专门的缓存相关元数据,如参数名称等。下表描述了提供给 context 的项目,以便你可以使用它们进行 key 和条件计算:

定位 说明 示例
methodName Root 对象 被调用的方法的名称 #root.methodName
method Root 对象 被调用的方法 #root.method.name
target Root 对象 被调用的目标对象 #root.target
targetClass Root 对象 被调用的目标的类 #root.targetClass
args Root 对象 用于调用目标的参数(以数组形式)。 #root.args[0]
caches Root 对象 运行当前方法所针对的缓存的集合 #root.caches[0].name
参数名称 Evaluation context 任何一个方法参数的名称。如果名称不可用(也许是由于没有 debug 信息),参数名称也可以在 #a<#arg> 下获得,其中 #arg 代表参数索引(从 0 开始)。 #iban or #a0(你也可以用 #p0#p<#arg> 的符号作为别名)。
result Evaluation context 方法调用的结果(要被缓存的值)。只在 unless 表达式、cache put 表达式(计算 key)或 cache evict 表达式(当 beforeInvocationfalse 时)中可用。对于支持的 wrapper(如 Optional),#result 指的是实际对象,而不是 wrapper。 #result

条件性缓存属性:condition和unless

有时,一个方法可能不适合一直被缓存(例如,它可能取决于给定的参数)。缓存注解通过 condition 参数支持这样的用例,condition 参数接受一个SpEL表达式,该表达式被评估为 truefalse。如果为 true,该方法被缓存。如果不是,它的行为就像该方法没有被缓存一样(也就是说,无论缓存中的值是什么或者使用了什么参数,该方法每次都被调用)。例如,只有当参数 name 的长度(length)短于 32 时,下面的方法才被缓存:

java 复制代码
@Cacheable(cacheNames="user", condition="#name.length() < 32") 
public User getUser(String name)

condition 属性使用的SpEL语言只有#root和获取参数类的SpEL表达式,不能使用返回结果的#result 。 所以 condition = "#result != null" 会导致所有对象都不进入缓存,每次操作都要经过数据库。

除了 condition 参数外,你还可以使用 unless 参数来否决 向缓存中添加值。与 condition 不同的是,unless 表达式是在方法被调用后进行评估的,并且只有表达式为false才会缓存结果。为了扩展前面的例子,也许我们只想缓存女性,就像下面的例子那样: gender的枚举值 0:男生 1:女生

java 复制代码
@Cacheable(cacheNames="user", condition="#name.length() < 32", unless="#result.gender==0") 
public User getUser(String name)

2.3 @CachePut

当需要更新缓存而不干扰方法的执行时,你可以使用 @CachePut 注解。也就是说,该方法总是被调用,其结果被放入缓存(根据 @CachePut 选项)。它支持与 @Cacheable 相同的选项,应该用于缓存的填充而不是方法流的优化。下面的例子使用了 @CachePut 注解:

java 复制代码
    @Transactional(rollbackFor = Exception.class)
    @CachePut(cacheNames = {"user", "order"}, key = "#user.id")
    public User updateUser(User user) {
        user.setCreateTime(LocalDateTime.now());
        userDAO.updateById(user);
        return user;
    }

2.4 @CacheEvict

缓存抽象不仅允许缓存存储,而且还允许驱逐。这个过程对于从缓存中移除陈旧或未使用的数据很有用。与 @Cacheable 相反,@CacheEvict 划分了执行缓存驱逐的方法(也就是说,作为从缓存中移除数据的触发器的方法)。与上面的注解类似,@CacheEvict 需要指定一个或多个受行动影响的缓存,允许指定一个自定义的缓存和 key 解析或条件,并具有一个额外的参数(allEntries),表明是否需要执行整个缓存的驱逐,而不仅仅是一个条目的驱逐(基于 key)。如下所示:

java 复制代码
    @Transactional(rollbackFor = Exception.class)
    @CacheEvict(cacheNames = "user", allEntries = true)
    public void batchSaveUser(List<User> users) {
        this.saveOrUpdateBatch(users);
    }

使用 allEntries 属性来驱逐缓存中的所有条目。当整个缓存区域需要被清除时,这个选项就派上用场了。与其逐出每个条目(这将花费很长的时间,因为它的效率很低),不如在一次操作中删除所有的条目,如前面的例子所示。注意,框架忽略了在这种情况下指定的任何 key,因为它并不适用(整个缓存被驱逐,而不是只有一个条目)。

你也可以通过使用 beforeInvocation 属性来表明驱逐应该发生在方法被调用之后(默认)还是之前。前者提供了与其他注解相同的语义: 一旦该方法成功完成,缓存上的一个动作(在这里是驱逐)就会被运行。如果该方法没有运行(因为它可能被缓存了)或者被抛出一个异常,那么驱逐就不会发生。后者( beforeInvocation=true)导致驱逐总是在方法被调用之前发生。这在驱逐不需要与方法结果相联系的情况下很有用。

请注意,void 方法可以和 @CacheEvict 一起使用---因为这些方法作为一个触发器,其返回值被忽略(因为它们不与缓存交互)。而 @Cacheable 则不是这样,它向缓存中添加数据或更新缓存中的数据,因此需要一个结果。

2.5 @Caching

有时,同一类型的多个注解(如 @CacheEvict@CachePut)需要被指定---例如因为不同缓存的 condition 或 key 表达式是不同的。@Caching 允许在同一个方法上使用多个嵌套的 @Cacheable@CachePut@CacheEvict 注解

java 复制代码
    @Transactional(rollbackFor = Exception.class)
    @Caching(put = {@CachePut(cacheNames = "user", key = "#user.id")},
             evict = {@CacheEvict(cacheNames = "user", key = "#user.userNo")})
    public User updateUser(User user) {
        user.setCreateTime(LocalDateTime.now());
        userDAO.updateById(user);
        return user;
    }

在同一个方法上使用 @CachePut@Cacheable 注解通常是非常不可取的,因为它们有不同的行为。后者会使方法的调用因使用缓存而被跳过,而前者则是为了运行缓存更新而强制调用。这导致了意想不到的行为,除了特定的角落案例(比如注解中的条件相互排斥),这种声明应该被避免。还要注意的是,这样的条件不应该依赖于结果对象(也就是 #result 变量),因为这些都是预先验证过的,以确认排除的情况。

2.6 @CacheConfig

到目前为止,我们已经看到缓存操作提供了许多自定义选项,而且你可以为每个操作设置这些选项。然而,如果一些自定义选项适用于类的所有操作,那么配置起来就会很繁琐。例如,为类的每个缓存操作指定使用的缓存名称,可以用一个类级的定义来代替。这就是 @CacheConfig 开始发挥作用的地方。下面的例子使用 @CacheConfig 来设置缓存的名称:

java 复制代码
@CacheConfig("user") 
public class userservice  {

    @Cacheable
    public user getUser(Long id) {...}
}

@CacheConfig 是一个类级注解,它允许共享缓存名称、自定义 KeyGenerator、自定义 CacheManager 和自定义 CacheResolver。把这个注解放在类上并不开启任何缓存操作。

一个操作级别的自定义总是覆盖 @CacheConfig 上的自定义设置。因此,这为每个缓存操作提供了三个层次的自定义:

  • 全局配置,可用于 CacheManagerKeyGenerator
  • 在类级别,使用 @CacheConfig
  • 在操作级别上。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

3.实现原理

经过上面使用案例讲解,你没有感觉Spring Cache注解的功能使用和Spring提供的声明式事务@Transactional套路是差不多的?是的,它们都是基于AOP代理实现的,关于声明式事务@Transactional请看之前的总结:Spring基于AOP事务控制实现原理Spring Boot项目业务代码中使用@Transactional事务失效踩坑点总结

接下来我们就浅浅地分析下流程吧:

先从@EnableCaching开启缓存说起

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {

	boolean proxyTargetClass() default false;

	AdviceMode mode() default AdviceMode.PROXY;

	int order() default Ordered.LOWEST_PRECEDENCE;

}

进入CachingConfigurationSelector:根据@EnableCaching配置的模式选择不同的配置类型,默认是PROXY模式,导入AutoProxyRegistrarProxyCachingConfiguration两个配置类。

java 复制代码
public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
   
   ....
     
   @Override
   public String[] selectImports(AdviceMode adviceMode) {
      switch (adviceMode) {
         case PROXY:
            return getProxyImports();
         case ASPECTJ:
            return getAspectJImports();
         default:
            return null;
      }
   }

   private String[] getProxyImports() {
      List<String> result = new ArrayList<>(3);
      result.add(AutoProxyRegistrar.class.getName());
      result.add(ProxyCachingConfiguration.class.getName());
      if (jsr107Present && jcacheImplPresent) {
         result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
      }
      return StringUtils.toStringArray(result);
   }

   private String[] getAspectJImports() {
      List<String> result = new ArrayList<>(2);
      result.add(CACHE_ASPECT_CONFIGURATION_CLASS_NAME);
      if (jsr107Present && jcacheImplPresent) {
         result.add(JCACHE_ASPECT_CONFIGURATION_CLASS_NAME);
      }
      return StringUtils.toStringArray(result);
   }

}

ProxyCachingConfiguration配置类核心逻辑如下:

java 复制代码
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {

	@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
		BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
		advisor.setCacheOperationSource(cacheOperationSource());
		advisor.setAdvice(cacheInterceptor());
		if (this.enableCaching != null) {
			advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
		}
		return advisor;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public CacheOperationSource cacheOperationSource() {
		return new AnnotationCacheOperationSource();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public CacheInterceptor cacheInterceptor() {
		CacheInterceptor interceptor = new CacheInterceptor();
		interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
		interceptor.setCacheOperationSource(cacheOperationSource());
		return interceptor;
	}

}

ProxyCachingConfiguration复用了父类的能力并且定了AOP的三个核心组件(Pointcut,Advice和Advisor)。AnnotationCacheOperationSource继承AbstractFallbackCacheOperationSource类实现CacheOperationSource接口,实现getCacheOperations方法将目标方法上缓存注解解析成缓存操作集合,方便后续使用。

最后来看看CacheInterceptor,它是一个 Advice,它实现了 MethodInterceptor 接口,我们主要看它作为一个 MethodInterceptorinvoke() 逻辑;

java 复制代码
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

	@Override
	@Nullable
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();

		CacheOperationInvoker aopAllianceInvoker = () -> {
			try {
				return invocation.proceed();
			}
			catch (Throwable ex) {
				throw new CacheOperationInvoker.ThrowableWrapper(ex);
			}
		};

		try {
      // 入口,CacheAspectSupport的执行方法
			return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
		}
		catch (CacheOperationInvoker.ThrowableWrapper th) {
			throw th.getOriginal();
		}
	}

}

CacheAspectSupport#execute()方法:

java 复制代码
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
		// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
		if (this.initialized) {
			Class<?> targetClass = getTargetClass(target);
			// 1.获取cacheOperationSource
			CacheOperationSource cacheOperationSource = getCacheOperationSource();
			if (cacheOperationSource != null) {
        // 获取对应注解配置信息
				Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
				if (!CollectionUtils.isEmpty(operations)) {
          // 执行
					return execute(invoker, method,
							new CacheOperationContexts(operations, method, args, target, targetClass));
				}
			}
		}

		return invoker.invoke();
	}

#execute()

java 复制代码
	private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Special handling of synchronized invocation
    // 注解用sync属性,进行加锁控制
		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, handleSynchronizedGet(invoker, key, cache));
				}
				catch (Cache.ValueRetrievalException ex) {
					// Directly propagate ThrowableWrapper from the invoker,
					// or potentially also an IllegalArgumentException etc.
					ReflectionUtils.rethrowRuntimeException(ex.getCause());
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}


		// Process any early evictions
    // 处理@CacheEvict的beforeInvocation为ture情况,即在方法执行前情况缓存
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// Check if we have a cached item matching the conditions
    // 处理@Cacheable
		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<>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && !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
    // 处理@CacheEvict的beforeInvocation为false情况,即在方法执行后情况缓存
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;
	}

4.总结

综上所述,Spring Cache 通过注解和抽象接口提供了灵活且简便的缓存管理方案,极大地简化了应用程序中缓存的使用和维护。通过注解的方式,可以很方便地在方法上启用缓存,减少了手动管理缓存的代码。Spring Cache 提供了多种缓存实现的支持,可以很方便地集成不同的缓存框架

相关推荐
初晴~21 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813626 分钟前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾1 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
黑胡子大叔的小屋1 小时前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark1 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
星就前端叭2 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
雷神乐乐2 小时前
Spring学习(一)——Sping-XML
java·学习·spring
小林coding3 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者3 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu