本篇文章主要讲解Spring当中@Cacheable缓存相关使用
在实际项目开发中,有些数据是变更频率比较低,但是查询频率比较高的,此时为了提升系统性能,可以使用缓存的机制实现,避免每次从数据库获取
第一步:使用@EnableCaching注解开启缓存
开启缓存功能,配置类中需要加上这个注解,有了这个注解之后,spring才知道你需要使用缓存的功能,其他和缓存相关的注解才会有效,Spring中主要是通过aop实现的,通过aop来拦截需要使用缓存的方法,实现缓存的功能
第二步:在方法或类上添加@Cacheable注解,表明某一个方法或者某一个类里的所有方法开启缓存功能;
@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的(因为是Aop实现的,Aop是核心是代理,内部调用无法被代理,也就不会生效)。@Cacheable可以指定三个属性,value、key和condition。
测试相关的类如下:ArticleService主要是提供模拟缓存的接口
package com.ym.example.demo.cachable;import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Component;import java.util.*;@Component
public class ArticleService {private Map<Long, String> articleMap = new HashMap<>();@Cacheable(cacheNames = {"cacheTest"})public List<String> list(){System.out.println("获取文章列表");return Arrays.asList("Spring", "MySQL", "java高并发", "Maven");}@Cacheable(value = {"cacheTest"})public List<String> listValue(){System.out.println("获取文章列表");return Arrays.asList("Spring", "MySQL", "java高并发", "Maven");}/*** @Author yangming* @Description* @Cacheable可以标记在方法上,也可以标记在一个类上,当标记在一个方法上是,表示该方法时支持缓存的,当标记到一个类上时,则表示该类所有的方法都是支持缓存的* 对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。* Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略* 这里需要注意,因为Spring缓存是通过aop实现的,aop又是依赖的代理模式,所以当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的* value和cacheNames一样,都是指定缓存的名称,这个cache名称可以是一个,也可以是多个,需要指定多个cache时其是一个数组* 可以将Cache理解为一个HashMap,系统中可以有很多歌Cache,每个Cache都有一个名字,你需要将方法的返回值放在哪个缓存中,需要通过缓存的名称来指定* key属性用来指定Spring缓存方法的返回结果时对应的key,因为Cache可以理解为一个HashMap,缓存以key-value的形式存储在HashMap中,value就是需要缓存的值(即方法返回值)* key属性支持spel表达式,当我们没有指定该属性时,Spring将使用默认策略生成key(org.springframework.cache.interceptor.SimpleKeyGenerator),默认会以方法参数创建key* 自定义策略是指我们可以通过SpEL表达式来指定我们的key,这里的SpEL表达式可以使用方法参数及他们对应的属性,使用方法参数时我们可以直接使用"#参数名"或者"#p参数index"* Spring还未我们提供了一个root对象可以用来生成key,通过该root对象我们可以获取到以下信息* methodName 当前方法名 #root.methodName* method 当前方法 #root.method.name* target 当前被调用的对象 #root.target* targetClass 当前被调用的对象的class #root.targetClass* args 当前方法参数组成的数组 #root.args[0]* caches 当前被调用的方法使用的cache #root.caches[0].name** @Date 2023/8/12 10:36* @param page* @param pageSize**/@Cacheable(value = {"cacheTest"}, key = "#root.target.class.name+'-'+#page+'-'+#pageSize")public String getPage(int page, int pageSize){String msg = String.format("page-%s-pageSize-%s", page, pageSize);System.out.println("从db中获取数据: " + msg);return msg;}/*** @Author yangming* @Description 没有指定key。默认为方法参数创建key,该方法的key为SimpleKey [1,10]* @Date 2023/8/12 10:55* @param page* @param pageSize**/@Cacheable(value = {"cacheTest"})public String getPageKey(int page, int pageSize){String msg = String.format("page-%s-pageSize-%s", page, pageSize);System.out.println("从db中获取数据: " + msg);return msg;}/*** @Author yangming* @Description 没有指定key。默认为方法参数创建key,该方法的key为SimpleKey []* @Date 2023/8/12 10:55**/@Cacheable(value = {"cacheTest"})public String getPageKey(){String msg = "测试key";System.out.println("从db中获取数据: " + msg);return msg;}/*** @Author yangming* @Description 有时候希望方法调用走缓存,有时候不希望走缓存,condition为true表示先尝试从缓存中取,如果缓存中没有,则执行方法,并将方法返回结果放到缓存中,* condition为false表示不走缓存,直接执行方法,并且返回的结果也不会放到缓存中* @Date 2023/8/11 19:39* @param id* @param cache**/@Cacheable(cacheNames = "cacheTest", key="'getById'+#id", condition = "#cache")public String getById(Long id, boolean cache){System.out.println("getById获取数据!");return "Spring缓存: " + UUID.randomUUID().toString();}/*** @Author yangming* @Description 当condition为空或者为true的情况下,unless才有效,condition为false的时候,unless无效,* unless为true,表示方法防结果不放到缓存中,unless为false,表示方法返回结果要放到缓存中* condition和unless对比* 缓存的使用过程中有两个点:* 1、查询缓存中是否有数据;* 2、如果缓存中没有数据,则去执行目标方法,然后将方法结果放到缓存中* Spring中通过condition和unless对这2点进行干预* condition作用在上面2个点的过程中,当为true的时候,会尝试从缓存中获取数据,如果没有,会执行方法,然后将方法返回值丢到缓存中;* 当为false的时候,则直接调用目标方法,并且结果不会放到缓存中* unless在condition为true的时候才有效,用来判断上面的第2点,看要不要将执行结果放到缓存中,* 如果为true,表示执行的结果不放到缓存中,* 如果为false,表示执行的结果要放到缓存中,在unless中可以使用spel表达式通过#result来获取方法返回值* @Date 2023/8/11 19:50* @param id**/@Cacheable(cacheNames = "cacheTest", key = "'findById'+#id", unless = "#result==null")public String findById(Long id){this.articleMap.put(1L, "Spring系列");System.out.println("-----获取文章: " + id + "-------");return this.articleMap.get(id);}/*** @Author yangming* @Description @CachePut也可以标注在类或者方法上,被标注的方法每次都会被调用,然后方法执行完毕之后,会将方法结果放到缓存中;当标注在类上,相当于在类的所有方法上都标注了@CachePut* @CachePut有3种情况,结果不会放到缓存* 1、当方法向外抛出异常的时候* 2、当condition的计算结果为false的时候* 3、unless的计算结果为true的时候* value,cacheNames,key,condition,unless的用法和@Cacheable中类似* @Date 2023/8/12 11:05* @param id* @param content**/@CachePut(cacheNames = "cacheTest", key = "'findById'+#id")public String add(Long id, String content){System.out.println("新增文章: " + id);this.articleMap.put(id, content);return content;}/*** @Author yangming* @Description @CacheEvict是用来清除缓存的,可以标注在类或者方法上,被标注在方法上,则目标方法被调用的时候,会清除指定的缓存;当标注在类上,相当于在类的所有方法上标注了@CacheEvict* value,cacheNames,key,condition的用法和@Cacheable中类似,@CacheEvict多了allEntries和beforeInvocation属性* allEntries:表示是否清理cacheNames指定的缓存中的所有缓存信息,默认为false* 可以将cache理解为一个HashMap,当allEntries为true的时候,相当于HashMap.clear(),* 当allEntries为false的时候,只会干掉key对应的数据,相当于HashMap.remove(key)* beforeInvocation:表示何时执行清除操作(方法执行前 or 方法执行成功之后)* true:表示@CacheEvict 标注的方法执行之前,执行清除操作* false:表示@CacheEvict 标注的方法执行成功之后,执行清除操作,当方法弹出异常的时候,不会执行清除操作* @Date 2023/8/12 11:25* @param id**/@CacheEvict(cacheNames = "cacheTest", key = "'findById'+#id")public void delete(Long id){System.out.println("根据id删除文章: " + id);this.articleMap.remove(id);}@Caching(cacheable = {@Cacheable(value = "cacheTest", key="#root.methodName")},put={@CachePut(value = "cacheTest", key = "#root.methodName")})public void testCaching(){}
}
CacheConfig提供缓存相关的配置,这个CacheMangager有多种实现,本例是使用的ConcurrentMapCacheManager实现,也可以是RedisCacheManager的实现
package com.ym.example.demo.cachable;import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;/*** @author yangming* @date 2023/8/11 19:24* @desc @EnableCaching表示开启缓存,有了这个注解之后,Spring才知道你需要使用缓存的功能,其他和缓存相关的注解才会生效* Spring中主要是通过aop实现的,通过aop来拦截需要使用缓存的方法,实现缓存的功能* @package com.ym.example.demo.cachable*/
@EnableCaching
@ComponentScan
@Configuration
public class CacheConfig {/*** @Author yangming* @Description 开启缓存之后,还需要在配置类中定义一个bean,作为缓存管理器,类型为CacheManager,* CacheManager是一个接口,有好几个实现,比如使用redis,ConcurrentMap为存储缓存信息,* 本例使用的是ConcurrentMapCacheManager,内部使用ConcurrentHashMap将缓存信息直接存储在本地jvm内存中* 不过线上环境一般是集群的方式,可以通过redis实现* @Date 2023/8/12 10:45**/@Beanpublic CacheManager cacheManager(){ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("cacheTest");return cacheManager;}
}
CacheTest是测试相关的方法
package com.ym.example.demo.cachable;import org.junit.Test;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;/*** @author yangming* @date 2023/8/11 19:26* @package com.ym.example.demo.cachable*/
public class CacheTest {@Testpublic void test(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);System.out.println(articleService.list());System.out.println(articleService.list());}执行结果:获取文章列表[Spring, MySQL, java高并发, Maven][Spring, MySQL, java高并发, Maven]@Testpublic void test1(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);//page=1,pageSize=10调用2次System.out.println(articleService.getPage(2, 10));System.out.println(articleService.getPage(2, 10));//page=2,pageSize=10调用2次System.out.println(articleService.getPage(3, 10));System.out.println(articleService.getPage(3, 10));{System.out.println("下面打印出cacheTest缓存中的key列表");ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);ConcurrentMapCache cacheTest = (ConcurrentMapCache) cacheManager.getCache("cacheTest");cacheTest.getNativeCache().keySet().stream().forEach(System.out::println);}}执行结果:从db中获取数据: page-2-pageSize-10page-2-pageSize-10page-2-pageSize-10从db中获取数据: page-3-pageSize-10page-3-pageSize-10page-3-pageSize-10下面打印出cacheTest缓存中的key列表com.ym.example.demo.cachable.ArticleService-3-10com.ym.example.demo.cachable.ArticleService-2-10@Testpublic void test2(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);//第一次,缓存中没有,执行方法,将结果放到缓存System.out.println(articleService.getById(1L, true));//第二次,缓存中有,直接从缓存中获取System.out.println(articleService.getById(1L, true));//第三次,condition为false,表示不从缓存取,直接执行方法,同时方法执行结果也不放到缓存System.out.println(articleService.getById(1L, false));//第四次,condition为true,缓存有,直接从缓存中取System.out.println(articleService.getById(1L, true));}执行结果:getById获取数据!Spring缓存: 1df6227d-53ae-46a6-9a70-a85e32e39f08Spring缓存: 1df6227d-53ae-46a6-9a70-a85e32e39f08getById获取数据!Spring缓存: 7ff6c668-87f1-4432-8844-61bf66b6e3efSpring缓存: 1df6227d-53ae-46a6-9a70-a85e32e39f08@Testpublic void test3(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);//第一次,没有缓存,执行方法,unless为false,表示执行结果要放到缓存中System.out.println(articleService.findById(1L));//第二次,第一次之后,缓存有数据,直接从缓存中取数据System.out.println(articleService.findById(1L));//第三次,缓存中没有,执行方法,result==null,unless为true,表示执行结果不放到缓存中System.out.println(articleService.findById(2L));//第四次,为了验证第三次的结论System.out.println(articleService.findById(2L));{System.out.println("下面打印出cacheTest缓存中的key列表");ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);ConcurrentMapCache cacheTest = (ConcurrentMapCache) cacheManager.getCache("cacheTest");cacheTest.getNativeCache().keySet().stream().forEach(System.out::println);}}执行结果:-----获取文章: 1-------Spring系列Spring系列-----获取文章: 2-------null-----获取文章: 2-------null下面打印出cacheTest缓存中的key列表findById1@Testpublic void test4(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);articleService.getPageKey(1,10);articleService.getPageKey();{System.out.println("下面打印出cacheTest缓存中的key列表");ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);ConcurrentMapCache cacheTest = (ConcurrentMapCache) cacheManager.getCache("cacheTest");cacheTest.getNativeCache().keySet().stream().forEach(System.out::println);}}执行结果:从db中获取数据: page-1-pageSize-10从db中获取数据: 测试key下面打印出cacheTest缓存中的key列表SimpleKey []SimpleKey [1,10]@Testpublic void test5(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);//增加2个文章,由于add方法上有@CachePut注解,所以新增之后会自动丢到缓存中articleService.add(1L, "java高并发系列");articleService.add(2L, "MySQL高手系列");//然后调用findById获取,看看是否会走缓存System.out.println("调用findById方法,会尝试从缓存中获取");System.out.println(articleService.findById(1L));System.out.println(articleService.findById(2L));{System.out.println("下面打印出cacheTest缓存中的key列表");ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);ConcurrentMapCache cacheTest = (ConcurrentMapCache) cacheManager.getCache("cacheTest");cacheTest.getNativeCache().keySet().stream().forEach(System.out::println);}}执行结果:新增文章: 1新增文章: 2调用findById方法,会尝试从缓存中获取java高并发系列MySQL高手系列下面打印出cacheTest缓存中的key列表findById2findById1@Testpublic void test6(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(CacheConfig.class);context.refresh();;ArticleService articleService = context.getBean(ArticleService.class);//第一次调用findById,缓存中没有,则调用方法,将结果丢到缓存中System.out.println(articleService.findById(1L));//第二次调用findById,缓存存在,直接从缓存中取System.out.println(articleService.findById(1L));//执行删除操作,delete方法上加了@CacheEvict注解,会清除缓存articleService.delete(1L);//再次调用findById方法,缓存中没有了,则会调用目标方法System.out.println(articleService.findById(1L));}执行结果:-----获取文章: 1-------Spring系列Spring系列根据id删除文章: 1-----获取文章: 1-------Spring系列
}
@Caching:缓存注解组
当我们在类上或者同一个方法上同时使用@Cacheable、@CachePut和@CacheEvic这几个注解中的多个的时候,此时可以使用@Caching这个注解来实现
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
@CacheConfig:提取公共配置
这个注解标注在类上,可以将其他几个缓存注解(@Cacheable、@CachePut和@CacheEvic)的公共参数给提取出来放在@CacheConfig中。比如当一个类中有很多方法都需要使用(@Cacheable、@CachePut和@CacheEvic)这些缓存注解的时候,大家可以看一下这3个注解的源码,他们有很多公共的属性,比如:cacheNames、keyGenerator、cacheManager、cacheResolver,若这些属性值都是一样的,可以将其提取出来,放在@CacheConfig中,不过这些注解(@Cacheable、@CachePut和@CacheEvic)中也可以指定属性的值对@CacheConfig中的属性值进行覆盖。
@CacheConfig(cacheNames = "cache1")
public class ArticleService {@Cacheable(key = "'findById'+#id")public String findById(Long id) {this.articleMap.put(1L, "spring系列");System.out.println("----获取文章:" + id);return articleMap.get(id);}
}
spring中的缓存主要是利用spring中aop实现的,通过Aop对需要使用缓存的bean创建代理对象,通过代理对象拦截目标方法的执行,实现缓存功能。重点在于 @EnableCaching 这个注解,可以从 @Import 这个注解看起
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {}
最终会给需要使用缓存的bean创建代理对象,并且会在代理中添加一个拦截器
org.springframework.cache.interceptor.CacheInterceptor ,这个类中的 invoke 方法是关键,
会拦截所有缓存相关的目标方法的执行,有兴趣可以去细看一下。