Spring缓存注解使用
? 从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.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.3</version>
</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;
@Configuration
@EnableCaching
public class CacheConfig {
private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
@Bean
public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory factory, @Autowired Jackson2JsonRedisSerializer serializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
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
.builder(factory)
.cacheDefaults(this.getRedisCacheConfigurationWithTtl(60, serializer))
.withInitialCacheConfigurations(this.getRedisCacheConfigurationMap(serializer))
.transactionAware()
.build();
return redisCacheManager;
}
@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;
}
@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();
};
}
@Bean
public Jackson2JsonRedisSerializer jacksonSerializer(){
Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
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;
}
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);
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
? @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);
}
@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();
};
}
@Cacheable(value = "c", keyGenerator = "myKeyGenerator")
@GetMapping("/brand")
public Result getBrand(@RequestParam("id") Long id){
return brandService.getBrand(id);
}
@CachePut
? @CachePut 也可以声明一个方法支持缓存功能。与 @Cacheable 不同的是使用 @CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
? 使用 @CachePut 可以指定的属性跟 @Cacheable 是一样的, @CachePut 适用于缓存更新。
@CacheEvict
? @CacheEvict 是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict 支持的属性额外增加了两个:
? 1、allEntries :是否需要清除缓存中的所有元素。默认为 false ,表示不需要。当指定了 allEntries 为 true 时,Spring Cache将忽略指定的key ,删除缓存中所有键;
? 2、beforeInvocation : 是否在方法执行成功之后触发键删除操作,默认是在对应方法成功执行之后触发的,若此时方法抛出异常而未能成功返回,不会触发清除操作。指定该属性值为 true 时,Spring会在调用该方法之前清除缓存中的指定元素。
@Caching
? @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 。
@CacheConfig
如果一个类中,多个方法都有同样的 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);
}
@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对象:
@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;
}
? 最后使用缓存注解:
@Cacheable(value = "ttl=12345678", key = "1000100")
@GetMapping("/test")
public Result test(){
return this.getAllBrand(new Page());
}
|