Spring-Cache因为支持多种缓存,每种缓存过期时间的设置方式无法统一,因此没有给出一个统一的处理方案,但是通过RedisCacheManager的源码可以找到解决方法.
先上一下效果:
@Cacheable(value = RedisKey.APP+"dashboardApi", cacheManager = "redisCacheManager")
@CacheExpire(60)
public List<NewAlertVo> newAlert(Integer type, String year){
//....略
}
通过增加@CacheExpire注解来实现设置过期时间.
要实现上述效果,我们需要重写一下RedisCacheManager
public class YourRedisCacheManager extends RedisCacheManager implements ApplicationContextAware, InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnextRedisCacheManager.class);
private ApplicationContext applicationContext;
private Map<String, RedisCacheConfiguration> initialCacheConfiguration = new LinkedHashMap<>();
/**
* key serializer
*/
public static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();
/**
* value serializer
* <pre>
* 使用 FastJsonRedisSerializer 会报错:java.lang.ClassCastException
* FastJsonRedisSerializer<Object> fastSerializer = new FastJsonRedisSerializer<>(Object.class);
* </pre>
*/
public static final GenericFastJsonRedisSerializer FASTJSON_SERIALIZER = new GenericFastJsonRedisSerializer();
/**
* key serializer pair
*/
public static final RedisSerializationContext.SerializationPair<String> STRING_PAIR = RedisSerializationContext
.SerializationPair.fromSerializer(STRING_SERIALIZER);
/**
* value serializer pair
*/
public static final RedisSerializationContext.SerializationPair<Object> FASTJSON_PAIR = RedisSerializationContext
.SerializationPair.fromSerializer(FASTJSON_SERIALIZER);
public ConnextRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
public Cache getCache(String name) {
Cache cache = super.getCache(name);
return new RedisCacheWrapper(cache);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() {
String[] beanNames = applicationContext.getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
final Class clazz = applicationContext.getType(beanName);
add(clazz);
}
super.afterPropertiesSet();
}
@Override
protected Collection<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList<>();
for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
caches.add(super.createRedisCache(entry.getKey(), entry.getValue()));
}
return caches;
}
private void add(final Class clazz) {
ReflectionUtils.doWithMethods(clazz, method -> {
ReflectionUtils.makeAccessible(method);
CacheExpire cacheExpire = AnnotationUtils.findAnnotation(method, CacheExpire.class);
if (cacheExpire == null) {
return;
}
Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class);
if (cacheable != null) {
add(cacheable.cacheNames(), cacheExpire);
return;
}
Caching caching = AnnotationUtils.findAnnotation(method, Caching.class);
if (caching != null) {
Cacheable[] cs = caching.cacheable();
if (cs.length > 0) {
for (Cacheable c : cs) {
if (cacheExpire != null && c != null) {
add(c.cacheNames(), cacheExpire);
}
}
}
} else {
CacheConfig cacheConfig = AnnotationUtils.findAnnotation(clazz, CacheConfig.class);
if (cacheConfig != null) {
add(cacheConfig.cacheNames(), cacheExpire);
}
}
}, method -> null != AnnotationUtils.findAnnotation(method, CacheExpire.class));
}
private void add(String[] cacheNames, CacheExpire cacheExpire) {
for (String cacheName : cacheNames) {
if (cacheName == null || "".equals(cacheName.trim())) {
continue;
}
long expire = cacheExpire.expire();
if(cacheExpire.overDay()) {
expire=getToTomorrowSeconds(new Date());
}
LOGGER.info("cacheName: {}, expire: {}", cacheName, expire);
if (expire >= 0) {
// 缓存配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(expire))
.disableCachingNullValues()
// .prefixKeysWith(cacheName)
.serializeKeysWith(STRING_PAIR)
.serializeValuesWith(FASTJSON_PAIR);
initialCacheConfiguration.put(cacheName, config);
} else {
LOGGER.warn("{} use default expiration.", cacheName);
}
}
}
protected static class RedisCacheWrapper implements Cache {
private final Cache cache;
RedisCacheWrapper(Cache cache) {
this.cache = cache;
}
@Override
public String getName() {
// LOGGER.info("name: {}", cache.getName());
try {
return cache.getName();
} catch (Exception e) {
LOGGER.error("getName ---> errmsg: {}", e.getMessage(), e);
return null;
}
}
@Override
public Object getNativeCache() {
// LOGGER.info("nativeCache: {}", cache.getNativeCache());
try {
return cache.getNativeCache();
} catch (Exception e) {
LOGGER.error("getNativeCache ---> errmsg: {}", e.getMessage(), e);
return null;
}
}
@Override
public ValueWrapper get(Object o) {
// LOGGER.info("get ---> o: {}", o);
try {
return cache.get(o);
} catch (Exception e) {
LOGGER.error("get ---> o: {}, errmsg: {}", o, e.getMessage(), e);
return null;
}
}
@Override
public <T> T get(Object o, Class<T> aClass) {
// LOGGER.info("get ---> o: {}, clazz: {}", o, aClass);
try {
return cache.get(o, aClass);
} catch (Exception e) {
LOGGER.error("get ---> o: {}, clazz: {}, errmsg: {}", o, aClass, e.getMessage(), e);
return null;
}
}
@Override
public <T> T get(Object o, Callable<T> callable) {
// LOGGER.info("get ---> o: {}", o);
try {
return cache.get(o, callable);
} catch (Exception e) {
LOGGER.error("get ---> o: {}, errmsg: {}", o, e.getMessage(), e);
return null;
}
}
@Override
public void put(Object o, Object o1) {
LOGGER.debug("put ---> o: {}, o1: {}", o, o1);
try {
cache.put(o, o1);
} catch (Exception e) {
LOGGER.error("put ---> o: {}, o1: {}, errmsg: {}", o, o1, e.getMessage(), e);
}
}
@Override
public ValueWrapper putIfAbsent(Object o, Object o1) {
LOGGER.debug("putIfAbsent ---> o: {}, o1: {}", o, o1);
try {
return cache.putIfAbsent(o, o1);
} catch (Exception e) {
LOGGER.error("putIfAbsent ---> o: {}, o1: {}, errmsg: {}", o, o1, e.getMessage(), e);
return null;
}
}
@Override
public void evict(Object o) {
// LOGGER.info("evict ---> o: {}", o);
try {
cache.evict(o);
} catch (Exception e) {
LOGGER.error("evict ---> o: {}, errmsg: {}", o, e.getMessage(), e);
}
}
@Override
public void clear() {
// LOGGER.info("clear");
try {
cache.clear();
} catch (Exception e) {
LOGGER.error("clear ---> errmsg: {}", e.getMessage(), e);
}
}
}
public static long getToTomorrowSeconds(Date currentTime) {
// 从一个 Instant和区域ID获得 LocalDateTime实例
LocalDateTime localDateTime = LocalDateTime.ofInstant(currentTime.toInstant(), ZoneId.systemDefault());
// 获取第第二天零点时刻的实例
LocalDateTime toromorrowTime = LocalDateTime.ofInstant(currentTime.toInstant(), ZoneId.systemDefault()).plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
// ChronoUnit日期枚举类,between方法计算两个时间对象之间的时间量
long seconds = ChronoUnit.SECONDS.between(localDateTime, toromorrowTime);
return seconds;
}
其中增加了启动就加载所有需要缓存的接口,不需要刻意注掉.还增加了在@CacheExpire注解中加入overDay的参数,表示存到当天结束.大家也可以根据自己的需要自行添加其他需要的内容.
重写了RedisCacheManager我们需要注入到容器中,因此需要写一个config,来继承CachingConfigurerSupport 代码如下:
@Configuration
@EnableCaching
@ConditionalOnProperty(name = "msf.redis.enable", havingValue = "true", matchIfMissing = true)
@Slf4j
public class YourRedisCacheConfig extends CachingConfigurerSupport {
private final RedisConnectionFactory redisConnectionFactory;
YourRedisCacheConfig (RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
@Bean("redisTemplate")
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// set key serializer
StringRedisSerializer serializer = YourRedisCacheManager.STRING_SERIALIZER;
// 设置key序列化类,否则key前面会多了一些乱码
template.setKeySerializer(serializer);
template.setHashKeySerializer(serializer);
// fastjson serializer
GenericFastJsonRedisSerializer fastSerializer = YourRedisCacheManager.FASTJSON_SERIALIZER;
template.setValueSerializer(fastSerializer);
template.setHashValueSerializer(fastSerializer);
// 如果 KeySerializer 或者 ValueSerializer 没有配置,则对应的 KeySerializer、ValueSerializer
// 才使用这个 Serializer
template.setDefaultSerializer(fastSerializer);
LettuceConnectionFactory factory = (LettuceConnectionFactory) redisConnectionFactory;
log.info("redis init...");
log.info("redis.factory: {}", redisConnectionFactory);
log.info("redis.host: {}", factory.getHostName());
// log.info("spring.redis.database: {}", factory.getDatabase());
// log.info("spring.redis.host: {}", factory.getHostName());
// log.info("spring.redis.port: {}", factory.getPort());
// log.info("spring.redis.timeout: {}", factory.getTimeout());
// log.info("spring.redis.password: {}", factory.getPassword());
// factory
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
@Bean
@Override
public KeyGenerator keyGenerator() {
return (o, method, objects) -> {
StringBuilder sb = new StringBuilder(32);
sb.append(o.getClass().getSimpleName());
sb.append(".");
sb.append(method.getName());
if (objects.length > 0) {
sb.append("#");
}
String sp = "";
for (Object object : objects) {
sb.append(sp);
if (object == null) {
sb.append("NULL");
} else {
sb.append(object.toString());
}
sp = ".";
}
return sb.toString();
};
}
/**
* 配置 RedisCacheManager,使用 cache 注解管理 redis 缓存
*/
// @Primary
@Bean(name = "redisCacheManager")
@Override
public CacheManager cacheManager() {
// 初始化一个RedisCacheWriter
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 设置默认过期时间:60s
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(60))
// .disableCachingNullValues()
// 使用注解时的序列化、反序列化
.serializeKeysWith(ConnextRedisCacheManager.STRING_PAIR).serializeValuesWith(ConnextRedisCacheManager.FASTJSON_PAIR);
return new YourRedisCacheManager(cacheWriter, defaultCacheConfig);
}
}
这里重写了keyGenerator,用指定的value+方法名+参数名组成key,可以实现同一个方法,不同的入参存不同的key的效果.
并且给CacheManager方法指定bean的名称为redisCacheManager,这样我们在用@Cacheable的时候指定这个manager就可以实现我们需要的效果了.
最后附上@CacheExpire注解的代码:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheExpire {
/**
* expire time, default 60s
*/
@AliasFor("expire")
long value() default 60L;
/**
* expire time, default 60s
*/
@AliasFor("value")
long expire() default 60L;
/**
*
* @Description:如果overDay=true 则存活时间到今天23点59分59秒
* @return:boolean
*/
boolean overDay() default false;
注:部分代码参考了互联网上前辈的代码,并非全部原创.