Spring boot 拾遗 —— Spring Cache 扩展 Duration

1 前言

在上一篇我们改写了 CacheManager 使得它能够解析 cacheName#duration 动态设置 TTL,现在我们将使用预定义的 CacheResolver 来让我们的代码能有下边的表现形式:  

Spring boot 拾遗 —— Spring Cache 扩展 Duration

 第一个方法在注解上规定了 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 的覆盖实现方法上

Spring boot 拾遗 —— Spring Cache 扩展 Duration

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 调用接口规定与实现

Spring boot 拾遗 —— Spring Cache 扩展 Duration
 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 }
Cache

 

Spring boot 拾遗 —— Spring Cache 扩展 Duration
 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 }
CaptchaCache

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 }

测试结果如期

Spring boot 拾遗 —— Spring Cache 扩展 Duration

 

上一篇:7.Null-safety


下一篇:使用AFNetWorking 上传文件/图片