1 前言
在上一篇我们改写了 CacheManager 使得它能够解析 cacheName#duration 动态设置 TTL,现在我们将使用预定义的 CacheResolver 来让我们的代码能有下边的表现形式:
第一个方法在注解上规定了 TTL 是 5 分钟, 第二个方法可以传入一个 duration 参数作为 TTL
2 DurationDetectCacheManager 的实现
作为 TTL 动态设置的基础,它的设计方式已在上一篇中涉及,因此我只贴出类的代码
1 package cn.pancc.spring.security.config.cache; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.boot.autoconfigure.cache.CacheProperties; 5 import org.springframework.data.redis.cache.RedisCache; 6 import org.springframework.data.redis.cache.RedisCacheConfiguration; 7 import org.springframework.data.redis.cache.RedisCacheManager; 8 import org.springframework.data.redis.cache.RedisCacheWriter; 9 import org.springframework.lang.Nullable; 10 import org.springframework.util.StringUtils; 11 12 import javax.annotation.Nonnull; 13 import java.time.Duration; 14 import java.time.format.DateTimeParseException; 15 import java.util.Optional; 16 17 /** 18 * @author pancc 19 */ 20 @Slf4j 21 public class DurationDetectCacheManager extends RedisCacheManager { 22 private static final String TAG = "#"; 23 private final CacheProperties cacheProperties; 24 25 public DurationDetectCacheManager(RedisCacheWriter cacheWriter, 26 RedisCacheConfiguration defaultCacheConfiguration, 27 CacheProperties cacheProperties) { 28 super(cacheWriter, defaultCacheConfiguration); 29 this.cacheProperties = cacheProperties; 30 } 31 32 @Nonnull 33 @Override 34 protected RedisCache createRedisCache(@Nonnull String name, @Nullable RedisCacheConfiguration cacheConfig) { 35 cacheConfig = cacheConfig == null ? RedisCacheConfiguration.defaultCacheConfig() : cacheConfig; 36 String[] array = StringUtils.delimitedListToStringArray(name, TAG); 37 name = array[0]; 38 if (array.length > 1) { 39 try { 40 Duration duration = Duration.parse(array[1]); 41 cacheConfig = cacheConfig.entryTtl(duration); 42 } catch (DateTimeParseException e) { 43 log.error("错误的 TTL 格式"); 44 cacheConfig = cacheConfig.entryTtl( 45 Optional.ofNullable(cacheProperties.getRedis().getTimeToLive()).orElse(Duration.ofSeconds(0))); 46 } 47 } 48 return super.createRedisCache(name, cacheConfig); 49 } 50 51 }
3 DurationDetectCacheResolver 的实现
3.1 规定
让我们对方法参数做如下规定, Duration 参数可以为 0 到多个, 但是至少存在一个的情况下,只取第一个解析为适应我们的 CacheManager 实现的 Cache
3.2 思路
CacheResolver#resolveCaches 方法中的参数 CacheOperationInvocationContext 我们可以获得当前调用方法上的所有 cacheNames ,可以获得方法入参,观察默认实现 AbstractCacheResolver 我们可以注意到最终进入获取 cache 是调用 CacheManager#getCache 的,实际上最终会走到我们的 CacheManager 的覆盖实现方法上
3.3 代码实现
1 package cn.pancc.spring.security.config.cache; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.cache.CacheManager; 5 import org.springframework.cache.interceptor.AbstractCacheResolver; 6 import org.springframework.cache.interceptor.BasicOperation; 7 import org.springframework.cache.interceptor.CacheOperationInvocationContext; 8 import org.springframework.util.StringUtils; 9 10 import javax.annotation.Nonnull; 11 import java.time.Duration; 12 import java.util.Collection; 13 import java.util.Set; 14 import java.util.stream.Collectors; 15 16 /** 17 * @author pancc 18 * @see org.springframework.cache.annotation.CachePut 19 * @see org.springframework.cache.annotation.Cacheable 20 * @see CacheConfig#cacheManager() 21 */ 22 @Slf4j 23 public class DurationDetectCacheResolver extends AbstractCacheResolver { 24 private static final String TAG = "#"; 25 26 public DurationDetectCacheResolver(@Nonnull CacheManager cacheManager) { 27 super(cacheManager); 28 } 29 30 @Override 31 protected Collection<String> getCacheNames(@Nonnull CacheOperationInvocationContext<?> context) { 32 final BasicOperation operation = context.getOperation(); 33 final Object[] args = context.getArgs(); 34 Set<String> cacheNames = operation.getCacheNames(); 35 if (args.length == 0) { 36 return cacheNames; 37 } 38 for (final Object o : args) { 39 if (Duration.class.isAssignableFrom(o.getClass())) { 40 final Duration duration = (Duration) o; 41 cacheNames = cacheNames.stream() 42 .map(this::removeTag) 43 .map(name -> this.appendDuration(name, duration)) 44 .collect(Collectors.toSet()); 45 } 46 } 47 log.debug("resolve cacheNames [{}] for method {}",cacheNames, context.getMethod()); 48 return cacheNames; 49 } 50 51 @Nonnull 52 private String appendDuration(@Nonnull String name, @Nonnull Duration duration) { 53 return name + TAG + duration.toString(); 54 } 55 56 @Nonnull 57 private String removeTag(@Nonnull String name) { 58 return StringUtils.delimitedListToStringArray(name, TAG)[0]; 59 } 60 }
3 合并配置与调用
3.1 缓存的顶层配置
1 package cn.pancc.spring.security.config.cache; 2 3 import com.fasterxml.jackson.databind.ObjectMapper; 4 import lombok.extern.slf4j.Slf4j; 5 import org.springframework.boot.autoconfigure.cache.CacheProperties; 6 import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 import org.springframework.cache.CacheManager; 8 import org.springframework.cache.annotation.CachingConfigurerSupport; 9 import org.springframework.cache.annotation.EnableCaching; 10 import org.springframework.cache.interceptor.CacheResolver; 11 import org.springframework.context.annotation.Bean; 12 import org.springframework.context.annotation.Configuration; 13 import org.springframework.data.redis.cache.RedisCacheConfiguration; 14 import org.springframework.data.redis.cache.RedisCacheWriter; 15 import org.springframework.data.redis.connection.RedisConnectionFactory; 16 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 17 import org.springframework.data.redis.serializer.RedisSerializationContext; 18 import org.springframework.data.redis.serializer.RedisSerializer; 19 20 import javax.annotation.Nonnull; 21 import java.time.Duration; 22 import java.util.Optional; 23 24 /** 25 * @author pancc 26 * @version 1.0 27 */ 28 @EnableCaching 29 @EnableConfigurationProperties(CacheProperties.class) 30 @Slf4j 31 @Configuration 32 public class CacheConfig extends CachingConfigurerSupport { 33 34 private final CacheProperties cacheProperties; 35 private final RedisConnectionFactory redisConnectionFactory; 36 private final ObjectMapper objectMapper; 37 38 public CacheConfig(CacheProperties cacheProperties, RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) { 39 this.cacheProperties = cacheProperties; 40 this.redisConnectionFactory = redisConnectionFactory; 41 this.objectMapper = objectMapper; 42 } 43 44 @Bean 45 @Override 46 public CacheManager cacheManager() { 47 return new DurationDetectCacheManager(redisCacheWriter(redisConnectionFactory), 48 redisCacheConfiguration(), 49 cacheProperties); 50 } 51 52 53 @Bean 54 public CacheResolver cacheResolver(@Nonnull CacheManager cacheManager) { 55 return new DurationDetectCacheResolver(cacheManager); 56 } 57 58 public RedisCacheWriter redisCacheWriter(@Nonnull RedisConnectionFactory redisConnectionFactory) { 59 return RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); 60 } 61 62 63 public RedisCacheConfiguration redisCacheConfiguration() { 64 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); 65 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); 66 if (redisProperties.getTimeToLive() != null) { 67 config = config.entryTtl(redisProperties.getTimeToLive()); 68 } 69 if (redisProperties.getKeyPrefix() != null) { 70 config = config.prefixKeysWith(redisProperties.getKeyPrefix()); 71 } 72 if (!redisProperties.isCacheNullValues()) { 73 config = config.disableCachingNullValues(); 74 } 75 if (!redisProperties.isUseKeyPrefix()) { 76 config = config.disableKeyPrefix(); 77 } 78 config = config.entryTtl(Optional.ofNullable(redisProperties.getTimeToLive()).orElse(Duration.ofSeconds(0))); 79 config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())); 80 config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))); 81 return config; 82 } 83 }
3.2 调用接口规定与实现
1 package cn.pancc.spring.security.cache; 2 3 import org.springframework.cache.annotation.CacheEvict; 4 import org.springframework.cache.annotation.CachePut; 5 import org.springframework.cache.annotation.Cacheable; 6 7 import javax.annotation.Nonnull; 8 import javax.annotation.Nullable; 9 import java.time.Duration; 10 11 /** 12 * @author pancc 13 */ 14 public interface Cache { 15 16 /** 17 * 从缓存中加载实体 18 * 19 * @param key 缓存 key 20 * @return 缓存的实体, 或者 null 当不在缓存中时 21 * @see Cacheable 22 */ 23 @Nullable 24 Object load(@Nonnull String key); 25 26 27 /** 28 * 缓存实体 29 * 30 * @param key 缓存的 key 31 * @param o 缓存实体 32 * @return 缓存实体 33 * @see CachePut 34 */ 35 @Nonnull 36 Object cache(@Nonnull String key, @Nonnull Object o); 37 38 39 /** 40 * 缓存实体 41 * 42 * @param key 缓存的 key 43 * @param o 缓存实体 44 * @param duration 缓存的过期时间 45 * @return 缓存实体 46 * @see CachePut 47 */ 48 @Nonnull 49 Object cache(@Nonnull String key, @Nonnull Object o, @Nonnull Duration duration); 50 51 52 /** 53 * 清除 key 对应的缓存 54 * 55 * @param key key 56 * @see CacheEvict 57 */ 58 void evict(@Nonnull String key); 59 60 /** 61 * 清除 namespace 下所有的缓存 62 * 63 * @see CacheEvict 64 */ 65 void flush(); 66 }
1 package cn.pancc.spring.security.cache; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.cache.annotation.CacheConfig; 5 import org.springframework.cache.annotation.CacheEvict; 6 import org.springframework.cache.annotation.CachePut; 7 import org.springframework.cache.annotation.Cacheable; 8 import org.springframework.stereotype.Component; 9 10 import javax.annotation.Nonnull; 11 import javax.annotation.Nullable; 12 import java.time.Duration; 13 14 /** 15 * @author pancc 16 */ 17 @Component 18 @Slf4j 19 @CacheConfig(cacheNames = "captcha",cacheResolver = "cacheResolver") 20 public class CaptchaCache implements Cache { 21 @CachePut(cacheNames = "captcha#PT5M", 22 key = "#uuid", unless = "#result == null") 23 @Override 24 @Nonnull 25 public Object cache(@Nonnull String uuid, @Nonnull Object code) { 26 return code; 27 } 28 29 @CachePut(key = "#uuid", unless = "#result == null") 30 @Nonnull 31 @Override 32 public Object cache(@Nonnull String uuid, @Nonnull Object code, @Nonnull Duration duration) { 33 return code; 34 } 35 36 @Cacheable( 37 key = "#uuid", unless = "#result==null") 38 @Nullable 39 @Override 40 public Object load(@Nonnull String uuid) { 41 return null; 42 } 43 44 45 @CacheEvict(key = "#key") 46 @Override 47 public void evict(@Nonnull String key) { 48 49 } 50 51 @CacheEvict(allEntries = true) 52 @Override 53 public void flush() { 54 55 } 56 }
3.3 测试类
1 package cn.pancc.spring.security.cache; 2 3 import cn.hutool.captcha.CaptchaUtil; 4 import cn.hutool.captcha.LineCaptcha; 5 import org.junit.jupiter.api.Test; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.boot.test.context.SpringBootTest; 8 9 import java.time.Duration; 10 import java.util.UUID; 11 12 @SpringBootTest 13 class CaptchaCacheTest { 14 @Autowired 15 private CaptchaCache captchaCache; 16 17 @Test 18 void cache() { 19 int count = 300; 20 for (int i = 0; i < count; i++) { 21 LineCaptcha captcha = CaptchaUtil.createLineCaptcha(120, 32, 5, 4); 22 String uuid = UUID.randomUUID().toString(); 23 String code = captcha.getCode(); 24 captchaCache.cache(uuid, code, Duration.ofDays(1)); 25 } 26 } 27 }
测试结果如期