【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache管理器的实战开发指南(修正篇)

前提介绍

Spring Cache是基于方法级别的,其核心思想是将调用带有缓存的方法时的参数和返回结果作为键值对存储在缓存中。当下次调用相同参数的方法时,直接从缓存中获取结果,而不再执行该方法。因此,在使用Spring Cache时,需要确保被缓存的方法对于相同的参数具有相同的返回结果。

使用Spring Cache需要进行两个步骤:

  1. 声明需要使用缓存的方法。
  2. 配置Spring对Cache的支持。

与Spring对事务管理的支持类似,Spring对Cache的支持有两种方式:基于注解和基于XML配置。下面我们先来看看基于注解的方式。

基于注解的支持

Spring提供了几个注解来支持Spring Cache,其中核心的注解包括@Cacheable和@CacheEvict。使用@Cacheable标记的方法在执行后,Spring Cache会缓存其返回结果,而使用@CacheEvict标记的方法可以在方法执行前或执行后移除Spring Cache中的某些元素。接下来,我们将详细介绍Spring基于注解对Cache的支持所提供的几个注解。

@Cacheable

@Cacheable注解可以应用于单个方法或整个类。当应用于方法时,该注解表示该方法支持缓存,当应用于类时表示该类的所有方法都支持缓存。被@Cacheable注解标记的方法会在调用后将返回值缓存起来,这样下次使用相同参数调用该方法时,可以直接从缓存中获取结果,无需再次执行该方法,从而提高方法的执行效率。

cacheable的属性介绍

Spring使用键值对来缓存方法的返回值。键用于检索缓存中的结果,而值则是方法的返回结果。Spring支持两种缓存键的策略:默认策略和自定义策略,接下来将对这两种策略进行详细说明。请注意,当一个支持缓存的方法在对象内部被调用时,不会触发缓存功能。

@Cacheable注解还提供了三个属性:value、key和condition。value属性用于指定缓存的名称或命名空间,可以用来区分不同缓存的作用域。key属性用于指定生成缓存键的条件,可以根据方法参数或其他条件生成不同的键,以实现更灵活的缓存策略。condition属性用于指定缓存的条件,只有在符合条件的情况下才会进行缓存。

value属性指定Cache名称

value属性是必须指定的,它表示当前方法的返回值会被缓存在哪个Cache上,也就是对应的Cache的名称。可以指定一个Cache,也可以指定多个Cache。当需要指定多个Cache时,可以使用数组形式。

java 复制代码
//Cache是发生在cache1上的
@Cacheable("cache1")
public User find(Integer id) {
   Return null;
}
 
 //Cache是发生在cache1和cache2上的
@Cacheable({"cache1", "cache2"})
public User find(Integer id) {
  Return null;
}
使用key属性自定义key

key属性用于指定Spring缓存方法返回结果时对应的key。该属性支持使用SpringEL表达式。如果未指定该属性,Spring将使用默认策略生成key。

下面是一个自定义策略的示例,我们可以使用SpringEL表达式来指定key。在EL表达式中,我们可以使用方法参数及其属性。使用方法参数作为key时,可以直接使用"#参数名"或者"#p参数索引"。

案例分析

以下是几个使用参数作为key的示例:

java 复制代码
  @Cacheable(value="users", key="#id")
   public User find(Integer id) {
      return null;
   }
 
   @Cacheable(value="users", key="#p0")
   public User find(Integer id) {
      return null;
   }
 
   @Cacheable(value="users", key="#user.id")
   public User find(User user) {
      return null;
   }
 
   @Cacheable(value="users", key="#p0.id")
   public User find(User user) {
      return null;
   }

除了上述使用方法参数作为key的方法外,Spring还提供了一个root对象,可以用来生成key。通过该root对象,我们可以获取以下信息:

  1. 方法名称(#root.methodName):可以获取当前缓存方法的名称。
  2. 目标类名称(#root.targetClass):可以获取当前缓存方法所在的目标类的名称。
  3. 目标对象(#root.target):可以获取当前缓存方法所在的目标对象。
  4. 方法参数(#root.args):可以获取当前缓存方法的参数列表。

使用这些信息作为key的一部分,可以更精确地指定缓存方法的返回结果所对应的key。下面是一个示例:

typescript 复制代码
@Cacheable(value = "myCache", key = "#root.methodName + ':' + #root.targetClass + ':' + #root.args[0]")
public String getSomeData(String key) {
    // ...
}

在这个示例中,使用了方法名称、目标类名称和第一个参数作为key的一部分,以确保每个不同的参数组合都能生成不同的缓存key。

注意:当我们要使用root对象的属性作为key时,我们也可以省略"#root"

caches

caches:当前被调用的方法使用的Cache,#root.caches[0].name,因为Spring默认使用的就是root对象的属性。下面是一个示例:

java 复制代码
@Cacheable(value = {"users", "xxx"}, key = "caches[1].name")
public User find(User user) {
    return null;
}

在这个示例中,我们使用了root对象的属性caches[1].name作为缓存的key。这意味着根据缓存的设置,当调用这个方法时,Spring将会使用usersxxx这两个缓存的名称,以及caches[1].name作为缓存的key。

condition属性指定发生的条件

有时候,我们可能不希望缓存一个方法的所有返回结果。通过condition属性,我们可以实现这一功能。condition属性的默认值为空,表示缓存所有的调用情况。该属性的值通过SpringEL表达式来指定,当为true时表示进行缓存处理,当为false时表示不进行缓存处理,即每次调用该方法时都会执行一次。下面的示例展示了只有当userid为偶数时才会进行缓存:

java 复制代码
@Cacheable(value = "users", key = "#user.id", condition = "#user.id % 2 == 0")
public User find(User user) {
    // 方法逻辑
}

在这个示例中,我们使用了SpringEL表达式#user.id % 2 == 0作为condition属性的值。这个表达式的含义是只有当userid是偶数时才会进行缓存。如果userid是奇数,则每次调用find()方法时,方法都会执行一次,而不会使用缓存的结果。

@CachePut

@CachePut注解也可以用来声明一个支持缓存功能的方法。与@Cacheable不同之处在于,@CachePut`注解用来标识一个支持缓存功能的方法。它不会在执行之前检查缓存条目,而是每次都执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

使用案例

@CachePut可以用于类和方法级别的标注。使用@CachePut时,我们可以像@Cacheable一样指定属性。

java 复制代码
@CachePut(value = "users") // 每次都会执行方法,并将结果存入指定的缓存中
public User find(Integer id) {
    return null;
}

@CacheEvict

allEntries属性

allEntries是一个布尔类型的属性,表示是否需要清除缓存中的所有元素。默认值为false,表示不需要清除所有元素。当将allEntries设置为true时,Spring Cache将忽略指定的key。有时,我们可以通过清除所有元素来提高效率,而不是逐个清除各个元素。

java 复制代码
@CacheEvict(value = "users", allEntries = true)
public void delete(Integer id) {
    System.out.println("delete user by id: " + id);
}

beforeInvocation属性

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation属性可以改变触发清除操作的时间,当将该属性值设置为true时,Spring会在调用该方法之前清除缓存中的指定元素。

java 复制代码
@CacheEvict(value = "users", beforeInvocation = true)
public void delete(Integer id) {
    System.out.println("delete user by id: " + id);
}

实际上,在使用Ehcache作为实现时,除了使用@CacheEvict来清除缓存元素之外,我们也可以通过配置Ehcache自身的驱除策略来实现。这可以通过Ehcache的配置文件来指定。

@Caching

@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。它拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

@Caching定义如下

java 复制代码
public @interface Caching {  
    Cacheable[] cacheable() default {}; //声明多个@Cacheable  
    CachePut[] put() default {};        //声明多个@CachePut  
    CacheEvict[] evict() default {};    //声明多个@CacheEvict  
}  

使用案例

java 复制代码
@Caching(cacheable = @Cacheable("users"), evict = {
    @CacheEvict("cache2"),
    @CacheEvict(value = "cache3", allEntries = true)
})
public User find(Integer id) {
    return null;
}

自定义缓存注解

上面介绍的@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中。

java 复制代码
@Caching(  
        put = {  
                @CachePut(value = "user", key = "#user.id"),  
                @CachePut(value = "user", key = "#user.username"),  
                @CachePut(value = "user", key = "#user.email")  
        }  
)  
@Target({ElementType.METHOD, ElementType.TYPE})  
@Retention(RetentionPolicy.RUNTIME)  
@Inherited  
public @interface WrapperCache {  
}  
@WrapperCache 
public User save(User user)  

Spring允许我们在配置可缓存的方法时使用自定义的注解,前提是自定义的注解上必须使用对应的注解进行标注。例如,我们可以创建一个使用@Cacheable进行标注的自定义注解,如下所示:

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(value = "users")
public @interface MyCacheable {

}

然后,我们可以在需要缓存的方法上使用@MyCacheable进行标注,以实现相同的效果。以下是优化后的示例代码:

java 复制代码
@MyCacheable
public User findById(Integer id) {
    System.out.println("find user by id: " + id);
    User user = new User();
    user.setId(id);
    user.setName("Name" + id);
    return user;
}

通过这样的设置,方法findById将会被缓存起来,以便在后续调用中直接返回缓存的结果。如果您还有其他问题或需要进一步帮助,请随时告诉我。

相关推荐
蓝澈112113 分钟前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_0720 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0232 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习37 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl1 小时前
深度解读jdk8 HashMap设计与源码
java
Falling421 小时前
使用 CNB 构建并部署maven项目
后端
guojl1 小时前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端