一般的,使用 spring cache 时,注解上不支持 ttl 过期时间
@Cacheable(cacheNames = "product3", key = "#id")
@GetMapping("/product3/{id}")
public String getById3(@PathVariable("id") Integer id) {
log.info("get from db");
return "success";
}
虽然可以在 配置 CacheManager 时进行配置,但每次调整都需要修改配置 文件
@Bean
public CacheManager cacheManager(LettuceConnectionFactory lettuceConnectionFactory ) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofMinutes(30L)) // 设置缓存的默认超时时间:30分钟
.disableCachingNullValues() // 如果是空值,不缓存
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())) // 设置key序列化器
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java())); // 设置value序列化器
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(lettuceConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
那如何实现注解上设置ttl呢?
思路:
acheManager.initializeCaches()
方法,重新加载继承 @Cacheable 注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Cacheable
@Documented
public @interface TTLCacheable {
@AliasFor(annotation = Cacheable.class, value = "value")
String[] value() default {};
@AliasFor(annotation = Cacheable.class, value = "cacheNames")
String[] cacheNames() default {};
@AliasFor(annotation = Cacheable.class, value = "key")
String key() default "";
@AliasFor(annotation = Cacheable.class, value = "keyGenerator")
String keyGenerator() default "";
@AliasFor(annotation = Cacheable.class, value = "cacheResolver")
String cacheResolver() default "";
@AliasFor(annotation = Cacheable.class, value = "condition")
String condition() default "";
@AliasFor(annotation = Cacheable.class, value = "unless")
String unless() default "";
@AliasFor(annotation = Cacheable.class, value = "sync")
boolean sync() default false;
/**
* cache 过期时间
* @return
*/
int ttl() default 0;
}
public class TTLCachePostProcessor implements BeanPostProcessor, SmartInitializingSingleton, BeanFactoryAware {
private BeanFactory beanFactory;
private static Set<TTLCacheable> cacheables = new ConcurrentHashSet<>();
private RedisTTLCacheConfig redisTTLCacheConfig;
public TTLCachePostProcessor(RedisTTLCacheConfig redisTTLCacheConfig) {
this.redisTTLCacheConfig = redisTTLCacheConfig;
}
@Override
public void afterSingletonsInstantiated() {
if (!CollectionUtils.isEmpty(cacheables)) {
CacheManager cm = beanFactory.getBean(CacheManager.class);
if (!(cm instanceof RedisCacheManager)){
return;
}
RedisCacheManager cacheManager = (RedisCacheManager) cm;
//反射
Field field = ReflectUtil.getField(RedisCacheManager.class, "initialCacheConfiguration");
field.setAccessible(Boolean.TRUE.booleanValue());
Map<String, RedisCacheConfiguration> configMap = (Map<String, RedisCacheConfiguration>) ReflectionUtils.getField(field, cacheManager);
Set<String> list = new HashSet<>();
for (TTLCacheable cacheable : cacheables) {
for (String cache : cacheable.cacheNames()) {
if (!list.contains(cache)) {
list.add(cache);
if (redisTTLCacheConfig!=null){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(redisTTLCacheConfig.getValueSerializationPair());
config = config.serializeKeysWith(redisTTLCacheConfig.getKeySerializationPair());
if (redisTTLCacheConfig.getKeyPrefix() != null) {
config = config.computePrefixWith(cacheName -> cacheName.concat(":").concat(redisTTLCacheConfig.getKeyPrefix()).concat(":"));
}
if (!redisTTLCacheConfig.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisTTLCacheConfig.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
config = config.entryTtl(Duration.ofSeconds(cacheable.ttl()));
configMap.put(cache, config);
}else {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.entryTtl(Duration.ofSeconds(cacheable.ttl()));
configMap.put(cache, config);
}
}
}
}
cacheManager.initializeCaches();
}
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(TTLCacheable.class))) {
Map<Method, TTLCacheable> annotatedMethods =
MethodIntrospector.selectMethods(targetClass, (MethodIntrospector.MetadataLookup<TTLCacheable>) method -> {
TTLCacheable ttlCacheables = AnnotatedElementUtils.getMergedAnnotation(method, TTLCacheable.class);
return ttlCacheables;
});
if (!annotatedMethods.isEmpty()) {
for (Map.Entry<Method, TTLCacheable> methodSetEntry : annotatedMethods.entrySet()) {
if (methodSetEntry.getValue().ttl() > 0) {
cacheables.add(methodSetEntry.getValue());
}
}
}
}
return bean;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
RedisTTLCacheConfig:
@Data
public class RedisTTLCacheConfig{
/**
* Entry expiration. By default the entries never expire.
*/
private Duration timeToLive;
/**
* Allow caching null values.
*/
private boolean cacheNullValues = true;
/**
* Key prefix.
*/
private String keyPrefix;
/**
* Whether to use the key prefix when writing to Redis.
*/
private boolean useKeyPrefix = true;
private ConversionService conversionService;
/**
* 序列化
*/
private RedisSerializationContext.SerializationPair<String> keySerializationPair;
private RedisSerializationContext.SerializationPair<Object> valueSerializationPair;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CacheTTLOperationConfigSelector.class)
public @interface EnableCacheTTLOperation {
}
@Configuration
@ConditionalOnClass(value = {RedisTemplate.class, CacheInterceptor.class})
@ConditionalOnBean(RedisTemplate.class)
@EnableConfigurationProperties(value = {CacheProperties.class})
public class CacheTTLAutoConfiguration {
@Bean
@ConditionalOnMissingBean(RedisTTLCacheConfig.class)
public RedisTTLCacheConfig redisTTLCacheConfig(CacheProperties properties, RedisTemplate redisTemplate) {
CacheProperties.Redis redisProperties = properties.getRedis();
RedisTTLCacheConfig config = new RedisTTLCacheConfig();
config.setTimeToLive(redisProperties.getTimeToLive());
config.setCacheNullValues(redisProperties.isCacheNullValues());
config.setKeyPrefix(redisProperties.getKeyPrefix());
config.setUseKeyPrefix(redisProperties.isUseKeyPrefix());
config.setKeySerializationPair(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getKeySerializer()));
config.setValueSerializationPair(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
return config;
}
@Bean
public TTLCachePostProcessor ttlCachePostProcessor(ObjectProvider<RedisTTLCacheConfig> provider) {
return new TTLCachePostProcessor(provider.getIfAvailable());
}
}
public class CacheTTLOperationConfigSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.x.x.CacheTTLAutoConfiguration"};
}
}
@EnableCacheTTLOperation
@EnableCaching
@EnableCacheTTLOperation
@SpringBootApplication
public class MvcApplication {
//omit...
}
Controller
@Slf4j
@RestController
public class CacheController {
@TTLCacheable(cacheNames = "product", key = "#id", ttl = 120)
@GetMapping("/product/{id}")
public String getById(@PathVariable("id") Integer id) {
log.info("get from db");
return "success";
}
@TTLCacheable(cacheNames = "product2", key = "#id")
@GetMapping("/product2/{id}")
public String getById2(@PathVariable("id") Integer id) {
log.info("get from db");
return "success";
}
@Cacheable(cacheNames = "product3", key = "#id")
@GetMapping("/product3/{id}")
public String getById3(@PathVariable("id") Integer id) {
log.info("get from db");
return "success";
}
}
good luck!