从3.1开始,Spring引入了对 Cache
的支持。
Spring Cache
包含两个顶级接口,Cache
(缓存)和 CacheManager
(缓存管理器)。Cache
接口包含缓存的各种操作集合,Cache
接口下 Spring 提供了各种 xxxCache 的实现,比如:RedisCache
、EhCache
、ConcurrentMapCache
。CacheManager
定义了创建、配置、获取、管理和控制多个唯一命名的 Cache
,这些 Cache
存在于 CacheManager
的上下文中。
public interface CacheManager {
@Nullable
Cache getCache(String var1);
Collection<String> getCacheNames();
}
针对不同的缓存技术,需要实现不同的 CacheManager
,Spring定义了如下的 cacheManger
实现:
1、SimpleCacheManager
:使用简单的 Collection
来存储缓存,主要用于测试;
2、ConcurrentMapCacheManager
:使用 ConcurrentMap
作为缓存技术(默认);
3、EhCacheCacheManager
:使用 EhCache
作为缓存技术;
4、GuavaCacheManager
:使用 google guava
的 GuavaCache
作为缓存技术;
5、RedisCacheManager
:使用Redis作为缓存技术(spring-data-redis
提供)。
下面以使用redis 为例子,配置CacheManager
:
1、引入相关依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.4.3version>
dependency>
2、开启缓存,配置缓存管理器
package com.jidi.spring.cache.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
* @Author jidi
* @Email [email protected]
* @Date 2021/11/21
*/
@Configuration
/** 开启缓存 */
@EnableCaching
public class CacheConfig {
/** 默认的java8日期时间格式 */
private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/** 默认的java8日期格式 */
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
/** 默认的java8时间格式 */
private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
/**
* Redis访问模板
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory factory, @Autowired Jackson2JsonRedisSerializer serializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// value的序列化方式采用jackson
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
// key都采用String的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 缓存管理器
*/
@Bean
public CacheManager cacheManager(@Autowired RedisConnectionFactory factory, @Autowired Jackson2JsonRedisSerializer serializer) {
// 初始化RedisCacheManager
RedisCacheManager redisCacheManager = RedisCacheManager
.builder(factory)
// 默认策略,未配置的 key 会使用这个
.cacheDefaults(this.getRedisCacheConfigurationWithTtl(60, serializer))
// 指定 key 策略
.withInitialCacheConfigurations(this.getRedisCacheConfigurationMap(serializer))
.transactionAware()
.build();
return redisCacheManager;
}
/**
* 自定义 CacheManager
*/
@Primary
@Bean("myRedisCacheManager")
public MyRedisCacheManager myRedisCacheManager(@Autowired RedisConnectionFactory factory, @Autowired Jackson2JsonRedisSerializer serializer){
MyRedisCacheManager myRedisCacheManager = new MyRedisCacheManager(
RedisCacheWriter.lockingRedisCacheWriter(factory),
this.getRedisCacheConfigurationWithTtl(60, serializer) );
return myRedisCacheManager;
}
/**
* 自定义 KeyGenerator
*/
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return (Object target, Method method, Object... params)-> {
StringBuffer sb= new StringBuffer();
sb.append(method.getName() + "-");
for (Object param : params) {
sb.append(param.toString()).append("_");
}
return sb.toString();
};
}
/**
* jacskson 序列化配置
*/
@Bean
public Jackson2JsonRedisSerializer jacksonSerializer(){
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 添加java8时间相关序列化/反序列化处理
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
objectMapper.registerModule(javaTimeModule);
jacksonSerializer.setObjectMapper(objectMapper);
return jacksonSerializer;
}
/**
* 获取redis配置
*/
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds, Jackson2JsonRedisSerializer serializer){
RedisCacheConfiguration configuration = RedisCacheConfiguration
.defaultCacheConfig()
// 设置序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
// 设置过期时间
.entryTtl(Duration.ofSeconds(seconds));
return configuration;
}
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(Jackson2JsonRedisSerializer serializer){
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(12);
// 自定义设置缓存时间,brand表示的是缓存注解中的 cacheNames/value
redisCacheConfigurationMap.put("brand", this.getRedisCacheConfigurationWithTtl(60*60, serializer));
return redisCacheConfigurationMap;
}
}
3、使用spring缓存
@RestController
@RequestMapping("/v1/web/brand")
public class BrandController {
@Autowired
private BrandService brandService;
@Cacheable(value = "brand", key = "#page.pageNum + '-' + #page.pageSize", condition = "#page.pageSize > 30")
@PostMapping("/allBrand")
public Result getAllBrand(@RequestBody Page page){
return brandService.getBrands(page);
}
/**
* 使用默认的配置和自定义的键生成策略
*/
@Cacheable(value = "c", keyGenerator = "myKeyGenerator")
@GetMapping("/brand")
public Result getBrand(@RequestParam("id") Long id){
return brandService.getBrand(id);
}
}
@Cacheable
可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。
@Cacheable
注解常用的属性:
1、cacheNames/value
:用来指定缓存组件的名字,相当于给生成的键指定一个前缀,例如:@Cacheable(value = "brand")
,在生成的键前面会添加brand::
;
2、key
:缓存数据时使用的 key
,会拼接在cacheNames/value
指定的缓存组件名后面,形成一个完整的键名,支持SPEL表达式;
3、keyGenerator
:key
的生成器,可以自己指定 key
的生成器,通过这个生成器来生成 key
;
4、cacheManager
:指定要使用的缓存管理器;
5、condition
:指定缓存数据的条件,默认为空,表示将缓存所有的调用情形。其值是通过SPEL表达式来指定的,当为 true
时表示进行缓存处理;当为 false
时表示不进行缓存处理;
6、unless
:当 unless
指定的条件为 true
,方法的返回值就不会被缓存;
7、sync
:是否使用异步模式。默认是方法执行完,以同步的方式将方法返回的结果存在缓存中。
使用SPEL定义key
自定义策略是指可以通过Spring的EL表达式来指定 key
。这里的EL表达式可以使用方法参数及它们对应的属性和返回结果。使用方法参数时可以直接使用#参数名
或者 #p参数index
,下面是几个使用参数作为 key
的示例:
@Cacheable(value = "brand", key = "#page.pageNum + '-' + #page.pageSize")
@PostMapping("/allBrand")
public Result getAllBrand(@RequestBody Page page){
return brandService.getBrands(page);
}
@Cacheable(value = "brand", key = "#id)
@PostMapping("/brand")
public Result getBrand(@RequestParam Long id){
return brandService.getBrand(id);
}
@Cacheable(value = "brand", key = "#p0 + '-' + #p1")
@PostMapping("/condition")
public Result getBrands(@RequestParam String name, Integer status){
return brandService.getBrands(name, status);
}
除了上述使用方法参数作为 key
之外,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 |
自定义keyGenerator
声明 @Cacheable
时不指定 key
参数,则该缓存名下的所有 key
会使用 KeyGenerator
根据参数自动生成。spring 有一个默认的 SimpleKeyGenerator
,在 spring boot 自动化配置中,这个会被默认注入。
spring 提供了 KeyGenerator
接口,支持根据场景自定义键生成器,只需要实现方法 generate
, 然后再使用缓存的时候指定键生成器为自定义的即可:
@FunctionalInterface
public interface KeyGenerator {
Object generate(Object var1, Method var2, Object... var3);
}
/**
* 自定义 KeyGenerator
*/
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return (Object target, Method method, Object... params)-> {
StringBuffer sb= new StringBuffer();
sb.append(method.getName() + "-");
for (Object param : params) {
sb.append(param.toString()).append("_");
}
return sb.toString();
};
}
/**
* 通过keyGenerator指明要使用的键生成器
*/
@Cacheable(value = "c", keyGenerator = "myKeyGenerator")
@GetMapping("/brand")
public Result getBrand(@RequestParam("id") Long id){
return brandService.getBrand(id);
}
@CachePut
也可以声明一个方法支持缓存功能。与 @Cacheable
不同的是使用 @CachePut
标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
使用 @CachePut
可以指定的属性跟 @Cacheable
是一样的, @CachePut
适用于缓存更新。
@CacheEvict
是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict
支持的属性额外增加了两个:
1、allEntries
:是否需要清除缓存中的所有元素。默认为 false
,表示不需要。当指定了 allEntries
为 true
时,Spring Cache将忽略指定的key
,删除缓存中所有键;
2、beforeInvocation
: 是否在方法执行成功之后触发键删除操作,默认是在对应方法成功执行之后触发的,若此时方法抛出异常而未能成功返回,不会触发清除操作。指定该属性值为 true
时,Spring会在调用该方法之前清除缓存中的指定元素。
@Caching
注解可以在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable
、put
和 evict
,分别用于指定@Cacheable
、@CachePut
和 @CacheEvict
。对于一个数据变动,更新多个缓存的场景,,可以通过 @Caching
来实现:
@Caching(cacheable = @Cacheable(cacheNames = "caching", key = "#age"), evict = @CacheEvict(cacheNames = "t4", key = "#age"))
public String caching(int age) {
return "caching: " + age + "-->" + UUID.randomUUID().toString();
}
上面这个就是组合操作:从 caching::#age
缓存取数据,不存在时执行方法并写入缓存;删除缓存 t4::#age
。
如果一个类中,多个方法都有同样的 cacheName
,keyGenerator
,cacheManager
和 cacheResolver
,可以直接使用 @CacheConfig
注解在类上声明,这个类中的方法都会使用@CacheConfig
属性设置的相关配置。
@Component
@CacheConfig(cacheNames = "mall_cache")
public class CacheComponent {
@Cacheable(key = "'perm-whitelist-'+#clientId", unless="#result == null")
public List<String> cacheWriteList(String clientId){
...
}
@Cacheable(key = "'perm-cutom-aci-' + #tenantId + '-' + #roleId + '-' + #tenantLevel + '-' + #subType", unless="#result == null")
public List<RequestDto> cacheRequest(Long tenantId,Long roleId,Integer tenantLevel,Integer subType){
...
}
}
之前的例子缓存的生效时间都是全局配置的,只要是同一个 cacheName
,使用的缓存策略都是一样的,实际场景中,可能即使是同一个 cacheName
,但是不同的 key
需要设置不同的失效时间。要实现这个场景,可以扩展一个自定义的 RedisCacheManager
,如:
public class MyRedisCacheManager extends RedisCacheManager {
public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
/**
* 重写 createRedisCache 方法
*/
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig){
String[] cells = StringUtils.delimitedListToStringArray(name, "=");
name = cells[0];
if (cells.length > 1) {
long ttl = Long.parseLong(cells[1]);
// 根据传参设置缓存失效时间
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}
return super.createRedisCache(name, cacheConfig);
}
}
然后在配置类,定义一个bean对象:
/**
* 自定义 CacheManager
*/
@Primary
@Bean("myRedisCacheManager")
public MyRedisCacheManager myRedisCacheManager(@Autowired RedisConnectionFactory factory, @Autowired Jackson2JsonRedisSerializer serializer){
MyRedisCacheManager myRedisCacheManager = new MyRedisCacheManager(
RedisCacheWriter.lockingRedisCacheWriter(factory),
this.getRedisCacheConfigurationWithTtl(60, serializer) );
return myRedisCacheManager;
}
最后使用缓存注解:
/**
* ttl=12345678,其中ttl为缓存名,12345678为存活时间
*/
@Cacheable(value = "ttl=12345678", key = "1000100")
@GetMapping("/test")
public Result test(){
return this.getAllBrand(new Page());
}