注解實現SpringCache自定義失效時間
SpringCache是一個很方便的緩存框架,但是官方提供的緩存的配置只有全局的緩存失效時間,沒有針對某個命名空間做配置,因為工作上業務的關系需要針對某一個緩存做單獨的控制,所有想了個辦法來實現。大概分為以下步驟:
1)自定義注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 緩存失效的注解,目前只支持在類級別上有效
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheExpire {
/**
* 失效時間,默認是60
* @return
*/
public long ttl() default 60L;
/**
* 單位,默認是秒
* @return
*/
public TimeUnit unit() default TimeUnit.SECONDS;
}
2)CacheManagerHelper獲得注解的值
import com.spboot.utils.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* CacheManager的輔助類
*/
@Component
public class CacheManagerHelper implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private static Map<String , Duration> CACHE_DURATION = new HashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
CacheManagerHelper.applicationContext = applicationContext;
}
/**
* 根據cacheName獲得對應的duration值
* @param name
* @return
*/
public static Duration getByKey(String name) {
return findAllCacheBean().get(name);
}
/**
* 找到所有的被 @CacheConfig 和 @CacheExpire 修飾的類對象
*/
public static Map<String , Duration> findAllCacheBean() {
if(CACHE_DURATION.size() == 0) {
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(CacheConfig.class);
if(beansWithAnnotation != null && beansWithAnnotation.size() > 0) {
for (Map.Entry<String, Object> entry : beansWithAnnotation.entrySet()) {
Object proxyObject = entry.getValue(); // 代理類
Object realObject = BeanUtils.getTarget(proxyObject); //獲得真實的對象
CacheExpire cacheExpire = realObject.getClass().getAnnotation(CacheExpire.class);
if(null != cacheExpire) {
CacheConfig cacheConfig = realObject.getClass().getAnnotation(CacheConfig.class);
String[] cacheNames = cacheConfig.cacheNames();
long convert = TimeUnit.SECONDS.convert(cacheExpire.ttl(), cacheExpire.unit());
Duration duration = Duration.ofSeconds(convert);
for (String cacheName : cacheNames) {
CACHE_DURATION.put(cacheName, duration);
}
}
}
}
}
return CACHE_DURATION;
}
}
3)修改源碼org.springframework.data.redis.cache.RedisCache
修改這里是為了改變每次存儲之前redis的key的ttl值,通過上面自定義的CacheManagerHelper來獲得。
修改源碼位置:
- org.springframework.data.redis.cache.RedisCache#put
- org.springframework.data.redis.cache.RedisCache#putIfAbsent
- 添加的方法:
- getDuration(java.lang.String, org.springframework.data.redis.cache.RedisCacheConfiguration)
因為代碼太長,只放出了被修改過的代碼,其余的保持不變:
/**
* 如果該命名空間使用了@CacheExpire注解就是用自定義的失效時間,否則使用默認的
* @param name
* @param cacheConfiguration
* @return
*/
private Duration getDuration(String name, RedisCacheConfiguration cacheConfiguration) {
Duration duration = CacheManagerHelper.getByKey(name);
if(null != duration) { // 如果當前命名空間配置了自定義失效時間,使用配置值
return duration;
}
return cacheConfig.getTtl(); // 否則使用全局的配置值
}
/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object)
*/
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException(String.format(
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
name));
}
// 修改的
cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), getDuration(name, cacheConfig));
// 默認的
// cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}
/*
* (non-Javadoc)
* @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object)
*/
@Override
public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
return get(key);
}
// 修改后的
byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), getDuration(name, cacheConfig));
// 默認的
// byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
// cacheConfig.getTtl());
if (result == null) {
return null;
}
return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
}
4)全局redis cache config
import java.time.Duration;
import org.springframework.cache.annotation.EnableCaching;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
@EnableCaching // 開啟spring的緩存
public class CacheConfig {
/**
* 自定義得緩存管理器
* @param redisConnectionFactory
* @return
*/
@Primary
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//初始化一個RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// key 序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// value的序列化機制
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jackson2JsonRedisSerializer();
// 配置
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 默認1個小時失效時間
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) // 設置 k v 序列化機制
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
//初始化RedisCacheManager
RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
return cacheManager;
}
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
return jackson2JsonRedisSerializer;
}
}
5)使用注解
假設在一個controller使用注解,例如:
@CacheExpire(ttl = 10, unit = TimeUnit.SECONDS) // 自定義注解,10秒鍾就過期
@CacheConfig(
cacheNames = "testApiService")
@RestController
public class TestApi {
@Cacheable
@GetMapping("/api/redis")
public Map<String,String> data() {
Map<String,String> map = new HashMap<String, String>();
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
return map;
}
}
如此一來就實現了使用注解控制緩存失效時間,這里還有優化的空間,比如注解精細到方法粒度的控制,使用aop來替代等,后面有時間再優化實踐吧。