前一篇文章(spring cache设置指定Key过期时间)已经提到过怎么实现spring cache自定义过期时间,但是拿来使用后,被吐槽不够优雅(也是醉了),也对,身为一个有追(不)求(服)心态的程序猿,就应该做到更好。
重新梳理后,大概思路是这样的:
- 自定义一个注解@TimeToLive,包括过期时间、和时间单位
- 方法上增加自定义注解@TimeToLive
- 定义一个切面,利用spring AOP原理,pointcut含有此自定义注解的方法
- 通过AOP切面拿到cacheName和Method的映射关系,缓存到本地
- 自定义CustomizerRedisCacheManager覆盖RedisCacheManager的computeExpiration方法
以下为代码部分:
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @Description Redis过期时间注解
* @Author huangd
* @Date 2020-01-02
**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TimeToLive {
long ttl() default 60 * 60;
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class VehicleSeriesCacheService {
@Autowired
private XXXMapper xxxMapper;
@Cacheable(value = "ota.vehicle.series", key = "#id + ''")
**@TimeToLive(ttl = 20, timeUnit = TimeUnit.MINUTES)**
public VehicleSeries fetchById(Long id) {
return xxxMapper.fetch(id);
}
}
import com.mng.boot.common.annotation.TimeToLive;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* @Description Spring cache 过期时间AOP处理
* @Author huangd
* @Date 2020-01-02
**/
@Aspect
@Component
public class CacheTtlAdvice implements Ordered {
private HashMap<String, Method> cacheNameMethodCache = new HashMap<>(16);
/**
* 初始化过期时间配置
* @param joinPoint 切入点
*/
@Before("@annotation(com.xiaopeng.ota.mng.boot.common.annotation.TimeToLive)")
public void before(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
TimeToLive timeToLive = AnnotationUtils.findAnnotation(method, TimeToLive.class);
Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class);
if(null != cacheable && cacheable.cacheNames().length > 0){
String[] cacheNames = cacheable.cacheNames();
if(timeToLive != null){
for(String cacheName : cacheNames){
if(!cacheNameMethodCache.containsKey(cacheName)){
cacheNameMethodCache.put(cacheName, method);
}
}
}
}
}
@Override
public int getOrder() {
return 1000;
}
public HashMap<String, Method> getCacheNameMethodCache() {
return cacheNameMethodCache;
}
}
@Slf4j
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
@Service
public class CustomizerRedisCacheManager extends RedisCacheManager {
@Autowired
private CacheTtlAdvice cacheTtlAdvice;
public CustomizerRedisCacheManager(@Qualifier("redisTemplate") RedisOperations redisOperations) {
super(redisOperations);
}
@Override
public long computeExpiration(String name) {
long defaultExpiration = super.computeExpiration(name);
Method method = cacheTtlAdvice.getCacheNameMethodCache().get(name);
if (method != null) {
TimeToLive timeToLive = AnnotationUtils.findAnnotation(method, TimeToLive.class);
return timeToLive.timeUnit().toSeconds(timeToLive.ttl());
}
return defaultExpiration;
}
}
配置RedisTemplate的key为StringRedisSerializer序列化方式(默认JDK序列化)
@Configuration
public class CustomCachingConfigurerSupport extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
说明:CacheTtlAdvice 切面类必须实现Ordered 接口并实现getOrder方法,因为@Cacheable也是通过AOP来实现的,会涉及到优先级问题,getOrder值越小越先执行,默认为(2147483647)可以查看@EnableCaching的order默认值,这里我们自定义的注解必须先执行。