前面简单实现了一个指定过期时间[SpringCache、Redis实现指定过期时间_csdn_Ty的博客-CSDN博客],但是是基于修改cacheName,对springCache设计改动太大,而且不能支持到期自动刷新,所以打算新增一个注解去配置过期时间、过期自动刷新时间。
1、新增自定义注解CacheExpireConfig
import java.lang.annotation.*;/*** @author tangzx*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheExpireConfig {/*** 缓存过期时间,支持单位天(d)、小时(h)、分钟(m)、秒钟(s)(不填单位默认秒)* 例:2h*/String expireTime() default "";/*** 缓存过期刷新时间,支持单位天(d)、小时(h)、分钟(m)、秒钟(s)(不填单位默认秒)* 例:2h*/String expireRefreshTime() default "";}
2、使用
@Override@CacheExpireConfig(expireTime = "60s", expireRefreshTime = "30s")@Cacheable(value = "testCache", condition = "#userId != null && #userName == null ")public String testCache(String userId, String userName) {System.out.println("=====================>");return "success";}
3、启动时加载缓存过期配置
import cn.hutool.core.lang.ClassScanner;
import com.yinhai.ta404.module.cache.annotation.CacheExpireConfig;
import com.yinhai.ta404.module.cache.redis.cache.MethodCacheExpireConfig;
import com.yinhai.ta404.module.cache.redis.cache.TaRedisCacheFactory;
import com.yinhai.ta404.module.cache.redis.util.DurationUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationListener;import java.lang.reflect.Method;
import java.util.Set;/*** @author Tzx* @date 2022/12/17 11:05*/
public class TaRedisCacheConfigListener implements ApplicationListener {@Overridepublic void onApplicationEvent(ApplicationPreparedEvent applicationPreparedEvent) {// 扫描所有类Set> classes = scanPackage();for (Class> target : classes) {Method[] methods = target.getMethods();for (Method method : methods) {// 如果方法上未同时注解@Cacheable和@CacheExpireConfig,不需要配置if (!method.isAnnotationPresent(Cacheable.class) || !method.isAnnotationPresent(CacheExpireConfig.class)) {continue;}Cacheable cacheable = method.getAnnotation(Cacheable.class);CacheExpireConfig cacheExpireConfig = method.getAnnotation(CacheExpireConfig.class);String expireTime = cacheExpireConfig.expireTime();String expireRefreshTime = cacheExpireConfig.expireRefreshTime();String[] cacheNames = ArrayUtils.addAll(cacheable.cacheNames(), cacheable.value());boolean autoRefresh = cacheExpireConfig.autoRefresh();for (String cacheName : cacheNames) {MethodCacheExpireConfig methodCacheExpireConfig = MethodCacheExpireConfig.builder().expireTime(DurationUtils.parseDuration(expireTime).getSeconds()).expireRefreshTime(DurationUtils.parseDuration(expireRefreshTime).getSeconds()).autoRefresh(autoRefresh).target(target).method(method).build();TaRedisCacheFactory.addCacheExpireConfig(cacheName, methodCacheExpireConfig);}}}}private Set> scanPackage() {// 使用的hutool的类扫描器,如果项目中未使用工具类,可自行实现return ClassScanner.scanPackage();}}
public static void main(String[] args) {SpringApplication application = new SpringApplicationBuilder().sources(StartApplication.class).build(args);try {application.addListeners(new TaRedisCacheConfigListener());application.run(args);} catch (Exception e) {e.printStackTrace();}}
4、重写RedisCacheManager,设置过期时间
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;import java.time.Duration;
import java.util.Map;/*** @author Tzx* @date 2022/12/13 19:33*/
public class TaRedisCacheManager extends RedisCacheManager {private static final Logger LOGGER = LoggerFactory.getLogger(TaRedisCacheManager.class);public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {super(cacheWriter, defaultCacheConfiguration);}public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {super(cacheWriter, defaultCacheConfiguration, initialCacheNames);}public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);}public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map initialCacheConfigurations) {super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);}public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map initialCacheConfigurations, boolean allowInFlightCacheCreation) {super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);}@Overrideprotected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {MethodCacheExpireConfig cacheable = TaRedisCacheFactory.getCacheExpireConfig(name);if (null != cacheable && cacheable.getExpireTime() > 0) {cacheConfig = entryTtl(name, cacheable.getExpireTime(), cacheConfig);}return super.createRedisCache(name, cacheConfig);}private RedisCacheConfiguration entryTtl(String cacheName, long ttl, @Nullable RedisCacheConfiguration cacheConfig) {Assert.notNull(cacheConfig, "RedisCacheConfiguration is required; it must not be null");cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));if (LOGGER.isDebugEnabled()) {LOGGER.debug("redisCache {} 过期时间为{}秒", cacheName, ttl);}return cacheConfig;}}
5、重写Cache的get方法,在get时如果不为空,检查是否需要刷新
@Overridepublic ValueWrapper get(@Nullable Object o) {if (null == o) {return null;}ValueWrapper wrapper = this.cache.get(o);// 刷新缓存if (null != wrapper) {SpringContextUtil.getApplicationContext().getBean(TaRedisCacheFactory.class).refreshCache(getName(),o.toString(), this::put);}return wrapper;}
6、TaRedisCacheFactory刷新
import com.alibaba.fastjson.JSON;
import com.yinhai.ta404.module.cache.redis.function.RefreshCacheFunction;
import com.yinhai.training.util.SpringContextUtil;
import com.yinhai.yyy.util.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.util.MethodInvoker;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;/*** @author Tzx* @date 2022/12/17 11:09*/
public class TaRedisCacheFactory {/*** 缓存过期配置*/private static final ConcurrentHashMap CACHE_EXPIRE_CONFIG = new ConcurrentHashMap<>();private static final Logger LOGGER = LoggerFactory.getLogger(TaRedisCacheFactory.class);public TaRedisCacheFactory() {// document why this method is empty}public static void addCacheExpireConfig(String cacheName, MethodCacheExpireConfig methodCacheExpireConfig) {CACHE_EXPIRE_CONFIG.put(cacheName, methodCacheExpireConfig);}public static MethodCacheExpireConfig getCacheExpireConfig(String cacheName) {return CACHE_EXPIRE_CONFIG.get(cacheName);}/*** 刷新缓存** @param cacheName 缓存名称* @param cacheKey 缓存key*/public void refreshCache(String cacheName, String cacheKey, RefreshCacheFunction f) {MethodCacheExpireConfig cacheable = getCacheExpireConfig(cacheName);if (null == cacheable) {return;}Class> targetClass = cacheable.getTarget();Method method = cacheable.getMethod();long expireRefreshTime = cacheable.getExpireRefreshTime();String redisKey = cacheName + cacheKey;long expire = RedisUtil.KeyOps.getExpire(redisKey);if (expire > expireRefreshTime) {return;}String argsStr = cacheKey.split("\\^")[1];Object[] args = JSON.parseObject(argsStr, Object[].class);if (null == args) {return;}try {// 创建方法执行器MethodInvoker methodInvoker = new MethodInvoker();methodInvoker.setArguments(args);methodInvoker.setTargetClass(targetClass);methodInvoker.setTargetMethod(method.getName());methodInvoker.setTargetObject(AopProxyUtils.getSingletonTarget(SpringContextUtil.getApplicationContext().getBean(targetClass)));methodInvoker.prepare();Object invoke = methodInvoker.invoke();//然后设置进缓存和重新设置过期时间f.put(cacheKey, invoke);RedisUtil.KeyOps.expire(cacheKey, cacheable.getExpireTime(), TimeUnit.SECONDS);} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {LOGGER.error("刷新缓存失败:" + e.getMessage(), e);}}}
7、MethodCacheExpireConfig
import lombok.Builder;
import lombok.Data;import java.lang.reflect.Method;/*** @author Tzx* @date 2022/12/17 11:10*/
@Data
@Builder
public class MethodCacheExpireConfig {/*** 缓存过期时间*/private long expireTime;/*** 缓存过期自动刷新阈值*/private long expireRefreshTime;/*** 是否自动刷新*/private boolean autoRefresh;/*** 类对象*/private Class> target;/*** 缓存方法*/private Method method;}
8、RefreshCacheFunction
/*** @author tangzx*/
@FunctionalInterface
public interface RefreshCacheFunction {/*** 缓存put** @param key key* @param value value*/void put(String key, Object value);}
9、DurationUtils
import java.time.Duration;/*** @author Tzx* @date 2022/12/17 12:04*/
public class DurationUtils {private DurationUtils(){// 2022/12/18}public static Duration parseDuration(String ttlStr) {String timeUnit = ttlStr.substring(ttlStr.length() - 1);switch (timeUnit) {case "d":return Duration.ofDays(parseLong(ttlStr));case "h":return Duration.ofHours(parseLong(ttlStr));case "m":return Duration.ofMinutes(parseLong(ttlStr));case "s":return Duration.ofSeconds(parseLong(ttlStr));default:return Duration.ofSeconds(Long.parseLong(ttlStr));}}private static long parseLong(String ttlStr) {return Long.parseLong(ttlStr.substring(0, ttlStr.length() - 1));}}