Spring Cache的使用教程:注解形式和api接口形式,以及调用内部方法注解失效的原因


在写程序的时候,对于频繁访问的数据,我们一般会使用缓存,将数据存储在内存中,方便下次直接在内存中读取,而不需要再去查询数据库或者再进行复杂运算得到。

Spring Cache 的使用

首先介绍一下Spring提供的Cache接口,并且提供了默认的实现 ConcurrentMapCache,看类名就知道,使用的ConcurrentMap实现。这里主要介绍一下注解形式。(另:个人更推荐接口的方式使用缓存,注解使用起来虽然方便,但是控制粒度不够,不能随时随地使用。当然需求简单的话注解就足够了)

注解形式的使用

首先看一下spring cache提供的注解,其实看名字就知道做什么了。这里主要简单介绍几个。
在这里插入图片描述

  • EnableCaching 开启缓存
    使用注解式缓存首先得在启动类或缓存配置类上添加该注解,才能启动缓存
  • Cacheable 使用缓存
    在方法上使用(或在类上,所有公用方法的返回都会被缓存)。当存在缓存时,直接从缓存中读取值并返回;不存在则调用方法,并将结果存入缓存中。该注解有以下常用属性:
    • value等同于cacheNames用于指定缓存名称;
    • key 指定缓存的key,可以使用spel表达式构建
    • keyGenerator(需要自己配置对应的bean)指定key的生成策略根据参数生成key,和key互斥
    • condition和unless都是指定条件下进行缓存,但unless是在返回之后判断是否进行缓存
    • cacheManager 指定自定义的缓存管理器(可以详细配置缓存的各种属性,过期时间,序列化方式什么的),都偷懒用注解形式了,一般也不会有什么特别要求吧
  • sync 是否同步,只允许同时一个线程对某个key进行读写,默认false
@Cacheable(cacheNames = "studentCache", key = "#args[0]+'_'+#args[1]", unless = "#result == null")
public Student getBySnoAndName(String sno, String name) {
    // 指定缓存名为 studentCache,key为 sno_name, 当返回为null时不缓存
}
  • CachePut 存缓入存
    同样在方法和类上使用,该注解只将方法的返回值存入缓存中,调用方法时不回从缓存中读取值。相关属性和 Cacheable 差不多,少一个 sync. 使用示例
@CachePut(cacheNames = "studentCache", key = "#student.sno+'_' +#student.name", condition = "#student!=null")
public Student addStudent(Student student) {
	// 将该方法的结果放入 studentCache 中, 条件 student != null
}
  • CacheEvict 使缓存失效
    作用域同方法和类。相关属性和上面的差不多,但还有另外两个
    • allEntries 是否所有的缓存的值失效,默认false
    • beforeInvocation 是否在调用前执行, 默认false,主要考虑存在方法报错的情况,false的话,如果方法报错,就不会执行。
@CacheEvict(cacheNames = "studentCache", key = "#sno +'_'+#name", beforeInvocation = true)
public boolean deleteById(String sno, String name) {
   // 执行该方法前将对应的缓存清除掉, 其实到这里大概就会发现注解式缓存的缺点了
   // 看方法名就知道,这个方法本来是根据id删除的,为了举例临时改了一下
   // 因为key是 sno_name (实际上sno作为key就够了,或者id也可以),但传入的参数为id,根本获取不到key。这就是注解的局限性
}
在同一个类调用加了缓存的方法失效的原因

注意 在使用注解的时候,在同一个类内部调用是会失效的!!!因为spring cacle注解使用aop实现的,实际上调用的是动态代理的方法。因为调用内部方法是有this的, 在生成代理类后,this就指向原对象了,而不会调用生成的代理对象的方法。

// 在内部调用方法,实际上是有this的,只是一般会省略。在生成代理类是 调用的还是原方法,而不是加了缓存的代理的方法
public Student getByStudent(Student stu) {
	return this.getBySnoAndName(stu.getSno(), stu.getName());
}

@Cacheable
public Student getBySnoAndName(String sno, String name) {
	// todo
}
  • Caching 将上述几个注解组合使用
    直接看源码吧,没啥讲的,就三个数组。
public @interface Caching {
	Cacheable[] cacheable() default {};
	CachePut[] put() default {};
	CacheEvict[] evict() default {};
}

// 使用
@Caching(cacheable = {@Cacheable(cacheNames = "stuCache1"),@Cacheable(cacheNames = "stuCache2")})

api 接口形式的使用

其实网上一搜 Spring cache 使用,大部分介绍注解方式的。所以我这里再介绍一下api接口形式。个人也更偏向于使用这种形式,功能性更强。直接上代码吧。

首先是定义一个 CacheManager 的bean,上面有提到。这里以RedisCacheManager 为例


@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
	
	// 缓存的配置信息
	RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHour(2));

    // 初始化的缓存, 可以指定相关的配置
    Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(4);
    configurationMap.put("studentCache", redisCacheConfiguration);

    RedisCacheManager cacheManager = RedisCacheManager.builder(redisCacheWriter)
            .withInitialCacheConfigurations(configurationMap)	// 加入初始化的自定义缓存配置
            .cacheDefaults(redisCacheConfiguration())       // 设置的默认配置,就都用同一个了
            .transactionAware()
            .build();

    return cacheManager;
}

然后使用缓存的时候需要使用redisCacheManager获取缓存对象


@Autowired
private RedisCacheManager redisCacheManager;

private Cache studentCache;

/**
 * 在初始化后调用该方法, 获取 studentCache 缓存
 * 注: Bean 初始化顺序, 构造方法 -> @Autowired -> @PostConstruct
 */
@PostConstruct
public void init() {
    studentCache = redisCacheManager.getCache("studentCache");
}

这样就可以直接在代码中使用缓存了。可以先看一下Spring Cache 接口提供的方法
在这里插入图片描述

  • ValueWrapper get(Object key); 根据key获取封装过的value,可以区分缓存中key是否存在
public StudentInfo getBySno(String sno) {
	Student s;
   	Cache.ValueWrapper wrapper = studentCache.get(sno);
    if (wrapper != null) {		// wrapper 为null,说明缓存中不存在
        s = (Student) wrapper.get();		// get也可能返回null,说明缓存中存了一个null
    } else {
    	s = getBySnoFromDB(sno);		// 缓存不存在查数据库
    	studentCache.put(sno, s);		// s 如果是null,上面wrapper就会get null
    }
    return s;
}
  • <T> T get(Object key, @Nullable Class<T> type); 返回指定类型的value,缓存不存在返回null
public StudentInfo getBySno(String sno) {
	Student s = studentCache.get(sno, Student.class);
    if (s == null) {		// 这个方法不回区分缓存不存在还是存入了null, 两种情况都是null
	  	s = getBySnoFromDB(sno);
	  	studentCache.put(sno, s);
	}
    return s;
}
  • <T> T get(Object key, Callable<T> valueLoader); 返会指定类型的value,如果缓存不存在,通过valueLoader获取,并存入缓存
public StudentInfo getBySno(String sno) {
    return studentCache.get(sno, sno -> getBySnoFromDB(sno));
}
  • void put(Object key, @Nullable Object value); 存入key-value缓存
  • ValueWrapper putIfAbsent(Object key, @Nullable Object value); 缓存不存在的时候存入,并返回null或之前的值
  • void evict(Object key); 清除指定key的缓存
  • void clear(); 清空所有的key
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值