注解实现SpringCache自定义失效时间(升级版)
之前做过注解实现自定义失效时间,但是需要重写spring-cache中的RedisCache源码,有些不怎么容易扩展,这里使用自定义的CacheManager、和RedisCache类来实现对应的逻辑:
旧版本链接
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获得注解的值
package com.spboot.config.cache.helper;
import com.spboot.config.cache.RedisCacheCustomize;
import com.spboot.utils.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 修改CacheManager的辅助类 调用时机: execute:314, CacheAspectSupport
* (org.springframework.cache.interceptor) invoke:61, CacheInterceptor
* (org.springframework.cache.interceptor) proceed:185,
* ReflectiveMethodInvocation (org.springframework.aop.framework) 由于 SpringAOP
* 只能切 Spring 容器管理的类,而调用时机本身就是使用反射来实现的 同是全局使用的是同一个 CacheManager,所以无法使用aop
*/
@Component
public class CacheManagerHelper implements ApplicationContextAware {
private final Logger logger = LoggerFactory.getLogger(CacheManagerHelper.class);
private static ApplicationContext applicationContext;
private static Map<String, Duration> CACHE_DURATION = new HashMap<>();
/** ttl字段 */
@Deprecated
private static Field CACHE_TTL;
/** ttl字段的修饰器 */
@Deprecated
private static Field CACHE_TTL_MODIFIERS;
static {
// 缓存以提高反射的性能
try {
CACHE_TTL = RedisCacheConfiguration.class.getDeclaredField("ttl");
CACHE_TTL.setAccessible(true);
// 获取字段修改器
CACHE_TTL_MODIFIERS = CACHE_TTL.getClass().getDeclaredField("modifiers");
CACHE_TTL_MODIFIERS.setAccessible(true);
} catch (Exception e) {
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
CacheManagerHelper.applicationContext = applicationContext;
}
/**
* 获得失效时间,不存在就按照默认的失效时间
* @param redisCacheCustomize
* @return
*/
public static Duration getByCache(RedisCacheCustomize redisCacheCustomize) {
Duration duration = getByKey(redisCacheCustomize.getName());
if( null == duration) {
return redisCacheCustomize.getCacheConfig().getTtl();
}
return duration;
}
/**
* 根据cacheName获得对应的duration值
*
* @param name
* @return
*/
public static Duration getByKey(String name) {
return findAllCacheBean().get(name);
}
/**
* 修改redisCache对象中持有的RedisCacheConfiguration的 private final Duration ttl;
*
* @param redisCache
*/
@Deprecated
public void modifyRedisCacheConfiguration(RedisCache redisCache) {
Duration duration = findAllCacheBean().get(redisCache.getName());
if (null == duration) {
return;
}
// 找到了就修改
modify(redisCache.getCacheConfiguration(), duration);
}
/**
* 修改RedisCacheConfiguration的private final Duration ttl;
*
* @param cacheConfiguration
* @param duration
*/
@Deprecated
private void modify(RedisCacheConfiguration cacheConfiguration, Duration duration) {
try {
// 去掉final修饰符
CACHE_TTL_MODIFIERS.setInt(CACHE_TTL, CACHE_TTL.getModifiers() & ~Modifier.FINAL);
// 修改字段值
CACHE_TTL.set(cacheConfiguration, duration);
// 添加final修饰符
CACHE_TTL_MODIFIERS.setInt(CACHE_TTL, CACHE_TTL.getModifiers() & ~Modifier.FINAL);
} catch (Exception e) {
logger.error("修改cacheConfiguration的ttl失败,原因:{}", e.getMessage());
}
}
/**
* 找到所有的被 @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)自定义RedisCacheCustomize
package com.spboot.config.cache;
import com.spboot.config.cache.helper.CacheManagerHelper;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.Assert;
public class RedisCacheCustomize extends RedisCache {
private final String name;
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration cacheConfig;
private final ConversionService conversionService;
protected RedisCacheCustomize(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
Assert.notNull(name, "Name must not be null!");
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
Assert.notNull(cacheConfig, "CacheConfig must not be null!");
this.name = name;
this.cacheWriter = cacheWriter;
this.cacheConfig = cacheConfig;
this.conversionService = cacheConfig.getConversionService();
}
public RedisCacheConfiguration getCacheConfig() {
return cacheConfig;
}
@Override
public void put(Object key, 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), CacheManagerHelper.getByCache(this));
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
return get(key);
}
byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
CacheManagerHelper.getByCache(this));
if (result == null) {
return null;
}
return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
}
private byte[] createAndConvertCacheKey(Object key) {
return serializeCacheKey(createCacheKey(key));
}
}
4)自定义RedisCacheManagerCustomize
package com.spboot.config.cache;
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;
public class RedisCacheManagerCustomize extends RedisCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
public RedisCacheManagerCustomize(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
// 使用自己的 RedisCacheCustomize
return new RedisCacheCustomize(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
}
5)缓存配置类
package com.spboot.config.cache;
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))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)) // 设置 k v 序列化机制
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
//初始化RedisCacheManager
// RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
// 重点在这里,使用自己的CacheManager
RedisCacheManager cacheManager = new RedisCacheManagerCustomize(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;
}
}
6) 使用注解
假设在一个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;
}
}