Spring Cache缓存注解

缓存名是必须提供的,可以使用引号、Value或者cacheNames属性来定义名称。下面的定义展示了users缓存的声明及其注解的使用:

@Cacheable(“users”)
//Spring 3.x
@Cacheable(value = “users”)
//Spring 从4.0开始新增了value别名cacheNames比value更达意,推荐使用
@Cacheable(cacheNames = “users”)
键生成器
缓存的本质就是键/值对集合。在默认情况下,缓存抽象使用(方法签名及参数值)作为一个键值,并将该键与方法调用的结果组成键/值对。 如果在Cache注解上没有指定key,
则Spring会使用KeyGenerator来生成一个key。

package org.springframework.cache.interceptor;
import java.lang.reflect.Method;

@FunctionalInterface
public interface KeyGenerator {
Object generate(Object var1, Method var2, Object… var3);
}
Sping默认提供了SimpleKeyGenerator生成器。Spring 3.x之后废弃了3.x 的DefaultKey
Generator而用SimpleKeyGenerator取代,原因是DefaultKeyGenerator在有多个入参时只是简单地把所有入参放在一起使用hashCode()方法生成key值,这样很容易造成key冲突。SimpleKeyGenerator使用一个复合键SimpleKey来解决这个问题。通过其源码可得知Spring生成key的规则。

/**

  • SimpleKeyGenerator源码的类路径参见{@link org.springframework.cache.interceptor.SimpleKeyGenerator}
    */
    从SimpleKeyGenerator的源码中可以发现其生成规则如下(附SimpleKey源码):

如果方法没有入参,则使用SimpleKey.EMPTY作为key(key = new SimpleKey())。
如果只有一个入参,则使用该入参作为key(key = 入参的值)。
如果有多个入参,则返回包含所有入参的一个SimpleKey(key = new SimpleKey(params))。
package org.springframework.cache.interceptor;

import java.io.Serializable;
import java.util.Arrays;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public class SimpleKey implements Serializable {
public static final SimpleKey EMPTY = new SimpleKey(new Object[0]);
private final Object[] params;
private final int hashCode;

public SimpleKey(Object... elements) {
    Assert.notNull(elements, "Elements must not be null");
    this.params = new Object[elements.length];
    System.arraycopy(elements, 0, this.params, 0, elements.length);
    this.hashCode = Arrays.deepHashCode(this.params);
}

public boolean equals(Object other) {
    return this == other || other instanceof SimpleKey && Arrays.deepEquals(this.params, ((SimpleKey)other).params);
}

public final int hashCode() {
    return this.hashCode;
}

public String toString() {
    return this.getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";
}

}
如需自定义键生成策略,可以通过实现org.springframework.cache.interceptor.KeyGenerator接口来定义自己实际需要的键生成器。示例如下,自定义了一个MyKeyGenerator类并且实现(implements)了KeyGenerator以实现自定义的键值生成器:

package com.example.cache.springcache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKey;
import java.lang.reflect.Method;

/**

  • @author: 博客「成猿手册」

  • @description: 为方便演示,这里自定义的键生成器只是在SimpleKeyGenerator基础上加了一些logger打印以区别自定义的Spring默认的键值生成器;
    */
    public class MyKeyGenerator implements KeyGenerator {

    private static final Logger logger = LoggerFactory.getLogger(MyKeyGenerator.class);

    @Override
    public Object generate(Object o, Method method, Object… objects) {
    logger.info(“执行自定义键生成器”);
    return generateKey(objects);
    }

    public static Object generateKey(Object… params) {
    if (params.length == 0) {
    logger.debug(“本次缓存键名称:{}”, SimpleKey.EMPTY);
    return SimpleKey.EMPTY;
    } else {
    if (params.length == 1) {
    Object param = params[0];
    if (param != null && !param.getClass().isArray()) {
    logger.debug(“本次缓存键名称:{}”, params);
    return param;
    }
    }
    SimpleKey simpleKey = new SimpleKey(params);
    logger.debug(“本次缓存键名称:{}”, simpleKey.toString());
    return simpleKey;
    }
    }
    }
    同时在Spring配置文件中配置:

使用示例如下:

@Cacheable(cacheNames = “userId”,keyGenerator = “myKeyGenerator”)
public User getUserById(String userId)
执行的打印结果如下:

first query…
14:50:29.901 [main] INFO com.example.cache.springcache.MyKeyGenerator - 执行自定义键生成器
14:50:29.902 [main] DEBUG com.example.cache.springcache.MyKeyGenerator - 本次键名称:test001
14:50:29.904 [main] INFO com.example.cache.springcache.MyKeyGenerator - 执行自定义键生成器
14:50:29.904 [main] DEBUG com.example.cache.springcache.MyKeyGenerator - 本次键名称:test001
query user by userId=test001
querying id from DB…test001
result object: com.example.cache.customize.entity.User@1a6c1270
second query…
14:50:29.927 [main] INFO com.example.cache.springcache.MyKeyGenerator - 执行自定义键生成器
14:50:29.927 [main] DEBUG com.example.cache.springcache.MyKeyGenerator - 本次键名称:test001
result object: com.example.cache.customize.entity.User@1a6c1270
@CachePut
@CachePut注解属性与@Cacheable注解属性相比少了sync属性。其他用法基本相同:

属性名 作用与描述
cacheNames/value 指定缓存的名字,缓存使用CacheManager管理多个缓存Cache,这些Cache就是根据该属性进行区分。对缓存的真正增删改查操作在Cache中定义,每个缓存Cache都有自己唯一的名字。
key 缓存数据时的key的值,默认是使用方法所有入参的值,可以使用SpEL表达式表示key的值。
keyGenerator 缓存的生成策略(键生成器),和key二选一,作用是生成键值key,keyGenerator可自定义。
cacheManager 指定缓存管理器(例如ConcurrentHashMap、Redis等)。
cacheResolver 和cacheManager作用一样,使用时二选一。
condition 指定缓存的条件(对参数判断,满足什么条件时才缓存),可用SpEL表达式,例如:方法入参为对象user则表达式可以写为condition = “#user.age>18”,表示当入参对象user的属性age大于18才进行缓存。
unless 否定缓存的条件(对结果判断,满足什么条件时不缓存),即满足unless指定的条件时,对调用方法获取的结果不进行缓存,例如:unless = “result==null”,表示如果结果为null时不缓存。
如果一个方法使用了@Cacheable注解,当重复(n>1)调用该方法时,由于缓存机制,并未再次执行方法体,其结果直接从缓存中找到并返回,即获取还的是第一次方法执行后放进缓存中的结果。

但实际业务并不总是如此,有些情况下要求方法一定会被调用,例如数据库数据的更新,系统日志的记录,确保缓存对象属性的实时性等等。

@CachePut注解就确保方法调用即执行,执行后更新缓存。

示例代码清单:

package com.example.cache.springcache;

import com.example.cache.customize.entity.User;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**

  • @author: 博客「成猿手册」

  • @description: com.example.cache.springcache
    */
    @Service(value = “userServiceBean2”)
    public class UserService2 {

    /**

    • 声明缓存名称为userCache
    • 缓存键值key未指定默认为userNumber+userName组合字符串
    • @param userId 用户Id
    • @return 返回用户对象
      */
      @Cacheable(cacheNames = “userCache”)
      public User getUserByUserId(String userId) {
      // 方法内部实现不考虑缓存逻辑,直接实现业务
      return getFromDB(userId);
      }

    /**

    • 注解@CachePut:确保方法体内方法一定执行,执行完之后更新缓存;
    • 使用与 {@link com.example.cache.springcache.UserService2#getUserByUserId(String)}方法
    • 相同的缓存userCache和key(缓存键值使用spEl表达式指定为userId字符串)以实现对该缓存更新;
    • @param user 用户参数
    • @return 返回用户对象
      */
      @CachePut(cacheNames = “userCache”, key = “(#user.userId)”)
      public User updateUser(User user) {
      return updateData(user);
      }

    private User updateData(User user) {
    System.out.println(“real updating db…” + user.getUserId());
    return user;
    }

    private User getFromDB(String userId) {
    System.out.println(“querying id from db…” + userId);
    return new User(userId);
    }
    }

测试代码清单:

package com.example.cache.springcache;

import com.example.cache.customize.entity.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**

  • @author: 博客「成猿手册」

  • @description: com.example.cache.springcache
    */
    public class UserMain2 {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext(“applicationContext.xml”);
    UserService2 userService2 = (UserService2) context.getBean(“userServiceBean2”);
    //第一次查询,缓存中没有,从数据库查询
    System.out.println(“first query…”);
    User user1 = userService2.getUserByUserId(“user001”);
    System.out.println("result object: " + user1);

     user1.setAge(20);
     userService2.updateUser(user1);
     //调用即执行,然后更新缓存
     user1.setAge(21);
     userService2.updateUser(user1);
    
     System.out.println("second query...");
     User user2 = userService2.getUserByUserId("user001");
     System.out.println("result object: " + user2);
     System.out.println("result age: " + user2.getAge());
    

    }
    }

测试打印结果如下:

first query…
querying id from db…user001
result object: com.example.cache.customize.entity.User@6d1ef78d
real updating db…user001
real updating db…user001
second query…
result object: com.example.cache.customize.entity.User@6d1ef78d
result age: 21

结果表明,执行了两次模拟调用数据库的方法。需要注意的是,在这个简单示例中,两次setAge()方法并不能够证明确实更新了缓存:把updateData()方法去掉也可以得到最终的用户年龄结果,因为set操作的仍然是getUserByName()之前获取的对象。

应该在实际操作中将getFromDB和updateData调整为更新数据库的具体方法,再通过加与不加@CachePut来对比最后的结果判断是否更新缓存。

@CacheEvict
@CacheEvict注解属性一览:

属性名 作用与描述
cacheNames/value 指定缓存的名字,缓存使用CacheManager管理多个缓存Cache,这些Cache就是根据该属性进行区分。对缓存的真正增删改查操作在Cache中定义,每个缓存Cache都有自己唯一的名字。
key 缓存数据时的key的值,默认是使用方法所有入参的值,可以使用SpEL表达式表示key的值。
keyGenerator 缓存的生成策略(键生成器),和key二选一,作用是生成键值key,keyGenerator可自定义。
cacheManager 指定缓存管理器(例如ConcurrentHashMap、Redis等)。
cacheResolver 和cacheManager作用一样,使用时二选一。
condition 指定删除缓存的条件(对参数判断,满足什么条件时才删除缓存),可用SpEL表达式,例如:入参为字符userId的方法删除缓存条件设定为当入参不是user001就删除缓存,则表达式可以写为condition = “!(‘user001’).equals(#userId)”。
allEntries allEntries是布尔类型的,用来表示是否需要清除缓存中的所有元素。默认值为false,表示不需要。当指定allEntries为true时,Spring Cache将忽略指定的key,清除缓存中的所有内容。
beforeInvocation 清除操作默认是在对应方法执行成功后触发的(beforeInvocation = false),即方法如果因为抛出异常而未能成功返回时则不会触发清除操作。使用beforeInvocation属性可以改变触发清除操作的时间。当指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@CacheEvict注解是@Cachable注解的反向操作,它负责从给定的缓存中移除一个值。大多数缓存框架都提供了缓存数据的有效期,使用该注解可以显式地从缓存中删除失效的缓存数据。该注解通常用于更新或者删除用户的操作。下面的方法定义从数据库中删除-一个用户,而@CacheEvict 注解也完成了相同的工作,从users缓存中删除了被缓存的用户。

在上面的实例中添加删除方法:

@CacheEvict(cacheNames = “userCache”)
public void delUserByUserId(String userId) {
//模拟实际业务中的删除数据操作
System.out.println(“deleting user from db…” + userId);
}

测试代码清单:

package com.example.cache.springcache;

import com.example.cache.customize.entity.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**

  • @author: 博客「成猿手册」

  • @description: com.example.cache.springcache
    */
    public class UserMain3 {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext(“applicationContext.xml”);
    UserService2 userService2 = (UserService2) context.getBean(“userServiceBean2”);
    String userId = “user001”;
    //第一次查询,缓存中没有,执行数据库查询
    System.out.println(“first query…”);
    User user1 = userService2.getUserByUserId(userId);
    System.out.println("result object: " + user1);

     //第二次查询从缓存中查询
     System.out.println("second query...");
     User user2 = userService2.getUserByUserId(userId);
     System.out.println("result object: " + user2);
    
     //先移除缓存再查询,缓存中没有,执行数据库查询
     userService2.delUserByUserId(userId);
     User user3 = userService2.getUserByUserId(userId);
     System.out.println("result object: " + user3);
    

    }
    }

执行的打印结果如下:

first query…
querying id from db…user001
result object: com.example.cache.customize.entity.User@6dee4f1b
second query…
result object: com.example.cache.customize.entity.User@6dee4f1b
deleting user from db…user001
querying id from db…user001
result object: com.example.cache.customize.entity.User@31bcf236

通过打印结果验证了@CacheEvict移除缓存的效果。需要注意的是,在相同的方法上使用@Caheable和@CacheEvict注解并使用它们指向相同的缓存没有任何意义,因为这相当于数据被缓存之后又被立即移除了,所以需要避免在同一方法上同时使用这两个注解。

@Caching
@Caching注解属性一览:

属性名 作用与描述
cacheable 取值为基于@Cacheable注解的数组,定义对方法返回结果进行缓存的多个缓存。
put 取值为基于@CachePut注解的数组,定义执行方法后,对返回方的方法结果进行更新的多个缓存。
evict 取值为基于@CacheEvict注解的数组。定义多个移除缓存。
总结来说,@Caching是一个组注解,可以为一个方法定义提供基于@Cacheable、@CacheEvict或者@CachePut注解的数组。

示例定义了User(用户)、Member(会员)和Visitor(游客)3个实体类,它们彼此之间有一个简单的层次结构:User是一个抽象类,而Member和Visitor类扩展了该类。

User(用户抽象类)代码清单:

package com.example.cache.springcache.entity;

/**

  • @author: 博客「成猿手册」

  • @description: 用户抽象类
    */
    public abstract class User {
    private String userId;
    private String userName;

    public User(String userId, String userName) {
    this.userId = userId;
    this.userName = userName;
    }
    //todo:此处省略get和set方法
    }

Member(会员类)代码清单:

package com.example.cache.springcache.entity;

import java.io.Serializable;

/**

  • @author: 博客「成猿手册」
  • @description: 会员类
    */
    public class Member extends User implements Serializable {
    public Member(String userId, String userName) {
    super(userId, userName);
    }
    }

Visitor(游客类)代码清单:

package com.example.cache.springcache.entity;

import java.io.Serializable;

/**

  • @author: 博客「成猿手册」

  • @description: 访客类
    */
    public class Visitor extends User implements Serializable {
    private String visitorName;

    public Visitor(String userId, String userName) {
    super(userId, userName);
    }
    }

UserService3类是一个Spring服务Bean,包含了getUser()方法。
同时声明了两个@Cacheable注解,并使其指向两个不同的缓存项: members和visitors。然后根据两个@Cacheable注解定义中的条件对方法的参数进行检查,并将对象存储在
members或visitors缓存中。

UserService3代码清单:

package com.example.cache.springcache;

import com.example.cache.springcache.entity.Member;
import com.example.cache.springcache.entity.User;
import com.example.cache.springcache.entity.Visitor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;

/**

  • @author: 博客「成猿手册」

  • @description: com.example.cache.springcache
    */
    @Service(value = “userServiceBean3”)
    public class UserService3 {

    private Map<String, User> users = new HashMap<>();

    {
    //初始化数据,模拟数据库中数据
    users.put(“member001”, new Member(“member001”, “会员小张”));
    users.put(“visitor001”, new Visitor(“visitor001”, “访客小曹”));
    }

    @Caching(cacheable = {
    /*
    该condition指定的SpEl表达式用来判断方法传参的类型
    instanceof是Java中的一个二元运算符,用来测试一个对象(引用类型)是否为一个类的实例
    */
    @Cacheable(value = “members”, condition = “#user instanceof T(” +
    “com.example.cache.springcache.entity.Member)”),
    @Cacheable(value = “visitors”, condition = “#user instanceof T(” +
    “com.example.cache.springcache.entity.Visitor)”)
    })
    public User getUser(User user) {
    //模拟数据库查询
    System.out.println(“querying id from db…” + user.getUserId());
    return users.get(user.getUserId());
    }
    }

UserService3类是-一个Spring服务Bean,包含了getUser()方法。同时声明了两个@Cacheable注解,并使其指向两个不同的缓存项: members 和visitors。
然后根据两个@Cacheable注解定义中的条件对方法的参数进行检查,并将对象存储在
members或visitors缓存中。

测试代码清单:

package com.example.cache.springcache;

import com.example.cache.springcache.entity.Member;
import com.example.cache.springcache.entity.User;
import com.example.cache.springcache.entity.Visitor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**

  • @author: 博客「成猿手册」

  • @description: com.example.cache.springcache
    */
    public class UserService3Test {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext(“applicationContext.xml”);
    UserService3 userService3 = (UserService3) context.getBean(“userServiceBean3”);

     Member member = new Member("member001", null);
    
     //会员第一次查询,缓存中没有,从数据库中查询
     User member1 = userService3.getUser(member);
     System.out.println("member userName-->" + member1.getUserName());
     //会员第二次查询,缓存中有,从缓存中查询
     User member2 = userService3.getUser(member);
     System.out.println("member userName-->" + member2.getUserName());
    
     Visitor visitor = new Visitor("visitor001", null);
     //游客第一次查询,缓存中没有,从数据库中查询
     User visitor1 = userService3.getUser(visitor);
     System.out.println("visitor userName-->" + visitor1.getUserName());
     //游客第二次查询,缓存中有,从缓存中查询
     User visitor2 = userService3.getUser(visitor);
     System.out.println("visitor userName-->" + visitor2.getUserName());
    

    }
    }
    深圳网站建设www.sz886.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值