spring cache 动态配置缓存过期时间

思路

  • 第 1 步

    • 自定义 CacheResolver
    • 通过 CacheOperationInvocationContext 获取到方法的参数名和参数值
    • 重写一个 cacheName(原始cacheName + 过期时间拼接在一起)
  • 第 2 步

    • 重写 CacheManger 的 getMissingCache 方法
    • 在 getMissingCache 方法中读取到第 1 步 cacheName 中过期时间
    • 初始化一个 cache 实例

问题

由于该实现是重新构建一个cacheName,这样会导致配合CachePutCacheEvict等使用时会有问题。

关键代码

Expired.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.tisson.common.redis.anno;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 自定义缓存过期时间配置注解,过期时间单位:s(秒)
* <p>
* 如果要使用 &#64;{@link Expired}注解,需要启用 &#64;{@link EnableTRedisConfiguration}
*
* @author YL
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Expired {
/**
* expire time, default 60s.
*/
long value() default 60;

/**
* Spring Expression Language (SpEL) expression for computing the expire time dynamically.
*
* <p>
* 与 {@link #value()} 属性互斥. 使用该属性配置的过期时间优先级比 {@link #value()} 属性高。由于该实现是重新构建一个cacheName,这样会导致配合CachePut、CacheEvict等使用时会有问题。
* </p>
*/
String spEl() default "";
}

TSimpleCacheResolver.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package cn.tisson.common.redis.cache;

import cn.tisson.common.redis.anno.Expired;
import cn.tisson.common.util.StrUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.AbstractCacheResolver;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
* 自定义 {@link org.springframework.cache.interceptor.CacheResolver} 以实现动态过期时间配置
*
* @author YL
*/
@Slf4j
public class TSimpleCacheResolver extends AbstractCacheResolver {
public TSimpleCacheResolver(CacheManager cacheManager) {
super(cacheManager);
}

private ExpressionParser parser = new SpelExpressionParser();
private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

@Override
protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
Method method = context.getMethod();
Object[] args = context.getArgs();
Set<String> cacheNames = context.getOperation().getCacheNames();
Expired expired = AnnotationUtils.findAnnotation(method, Expired.class);
if (expired == null || StrUtils.isBlank(expired.spEl())) {
return cacheNames;
}
// Shortcut if no args need to be loaded
if (ObjectUtils.isEmpty(args)) {
return cacheNames;
}
// Expose indexed variables as well as parameter names (if discoverable)
String[] paramNames = this.discoverer.getParameterNames(method);
int paramCount = (paramNames != null ? paramNames.length : method.getParameterCount());
int argsCount = args.length;

EvaluationContext eval = new StandardEvaluationContext();
for (int i = 0; i < paramCount; i++) {
Object value = null;
if (argsCount > paramCount && i == paramCount - 1) {
// Expose remaining arguments as vararg array for last parameter
value = Arrays.copyOfRange(args, i, argsCount);
} else if (argsCount > i) {
// Actual argument found - otherwise left as null
value = args[i];
}
/**
* see {@link MethodBasedEvaluationContext#lazyLoadArguments()}
*/
eval.setVariable("a" + i, value);
eval.setVariable("p" + i, value);
if (paramNames != null) {
eval.setVariable(paramNames[i], value);
}
}
Expression expression = parser.parseExpression(expired.spEl());
Long ttl = expression.getValue(eval, Long.class);
if (ttl != null && ttl <= 0) {
return cacheNames;
}
Set<String> names = new HashSet<>();
for (String cacheName : cacheNames) {
names.add(cacheName + ".exp_" + ttl);
}
return names;
}
}

TRedisCacheManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package cn.tisson.common.redis.cache;

import cn.tisson.common.redis.anno.Expired;
import cn.tisson.common.redis.util.CacheUtils;
import cn.tisson.common.util.StrUtils;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
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.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ReflectionUtils;

import java.time.Duration;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* 自定义 RedisCacheManager 缓存管理器
* <pre>
* support spring-data-redis 1.8.10.RELEASE
* not support spring-data-redis 2.0.0.RELEASE +
* </pre>
*
* @author YL
*/
@Slf4j
public class TRedisCacheManager extends RedisCacheManager implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;

private Map<String, RedisCacheConfiguration> initialCacheConfiguration = new LinkedHashMap<>();

/**
* key serializer
*/
public static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();

/**
* value serializer
* <pre>
* 使用 FastJsonRedisSerializer 会报错:java.lang.ClassCastException
* FastJsonRedisSerializer<Object> fastSerializer = new FastJsonRedisSerializer<>(Object.class);
* </pre>
*/
public static final GenericFastJsonRedisSerializer FASTJSON_SERIALIZER = new GenericFastJsonRedisSerializer();

/**
* key serializer pair
*/
public static final RedisSerializationContext.SerializationPair<String> STRING_PAIR = RedisSerializationContext
.SerializationPair.fromSerializer(STRING_SERIALIZER);
/**
* value serializer pair
*/
public static final RedisSerializationContext.SerializationPair<Object> FASTJSON_PAIR = RedisSerializationContext
.SerializationPair.fromSerializer(FASTJSON_SERIALIZER);

@Getter
private RedisCacheConfiguration defaultCacheConfig;

public TRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfig) {
super(cacheWriter, defaultCacheConfig);
this.defaultCacheConfig = defaultCacheConfig;
}

// public TedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
// Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
// super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
// }

@Override
public Cache getCache(String name) {
Cache cache = super.getCache(name);
return new TRedisCacheWrapper(cache);
}

@Override
protected RedisCache getMissingCache(String name) {
RedisCacheConfiguration config = getRedisCacheConfiguration(name, computeTtl(name));
return super.createRedisCache(name, config);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

@Override
public void afterPropertiesSet() {
String[] beanNames = applicationContext.getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
final Class<?> clazz = applicationContext.getType(beanName);
doWith(clazz);
}
super.afterPropertiesSet();
}

@Override
protected Collection<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList<>();
for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
caches.add(super.createRedisCache(entry.getKey(), entry.getValue()));
}
return caches;
}

private void doWith(final Class<?> clazz) {
ReflectionUtils.doWithMethods(clazz, method -> {
ReflectionUtils.makeAccessible(method);
Expired expired = AnnotationUtils.findAnnotation(method, Expired.class);
Cacheable cacheable = AnnotationUtils.findAnnotation(method, Cacheable.class);
Caching caching = AnnotationUtils.findAnnotation(method, Caching.class);
CacheConfig cacheConfig = AnnotationUtils.findAnnotation(clazz, CacheConfig.class);

List<String> cacheNames = CacheUtils.getCacheNames(cacheable, caching, cacheConfig);
add(cacheNames, expired);
}, method -> null != AnnotationUtils.findAnnotation(method, Expired.class));
}

private void add(List<String> cacheNames, Expired expired) {
for (String cacheName : cacheNames) {
if (cacheName == null || "".equals(cacheName.trim())) {
continue;
}
long expire = expired.value();
if (log.isDebugEnabled()) {
log.debug("cacheNames: {}, expire: {}s", cacheNames, expire);
}
if (expire >= 0) {
RedisCacheConfiguration config = getRedisCacheConfiguration(cacheName, Duration.ofSeconds(expire));
initialCacheConfiguration.put(cacheName, config);
} else {
log.warn("{} use default expiration.", cacheName);
}
}
}

private RedisCacheConfiguration getRedisCacheConfiguration(String cacheName, Duration ttl) {
boolean allowCacheNullValues = defaultCacheConfig.getAllowCacheNullValues();
boolean useKeyPrefix = defaultCacheConfig.usePrefix();
String keyPrefix = defaultCacheConfig.getKeyPrefixFor(cacheName);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(ttl)
.serializeKeysWith(TRedisCacheManager.STRING_PAIR)
.serializeValuesWith(TRedisCacheManager.FASTJSON_PAIR);
if (useKeyPrefix && StrUtils.isNotBlank(keyPrefix)) {
config = config.prefixKeysWith(keyPrefix);
}
if (!allowCacheNullValues) {
config = config.disableCachingNullValues();
}
return config;
}

Pattern pattern = Pattern.compile("\\.exp_(\\d+)");

private Duration computeTtl(String cacheName) {
Matcher matcher = pattern.matcher(cacheName);
if (matcher.find()) {
return Duration.ofSeconds(Long.parseLong(matcher.group(1)));
}
return defaultCacheConfig.getTtl();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// RedisService.java
@Cacheable(
value = "redis.v1.service",
condition = "#cache eq true",
unless = "#result == null or #result.empty")
@Expired(spEl = "#id")
// @Expired(spEl = "#p1")
// @Expired(spEl = "#a1")
public Map<String, Object> get(boolean cache, Long id) {

Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", "name-" + id);

log.info("map: {}", map);
return map;
}

// RedisServiceTest.java
@Test
public void testGet() {
Map<String, Object> map = redisService.get(true, 1234L);
log.info("map ---> {}", map);
}
  • 本文作者: forever杨
  • 本文链接: https://blog.yl-online.top/posts/23adfb31.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。如果文章内容对你有用,请记录到你的笔记中。本博客站点随时会停止服务,请不要收藏、转载!