Skip to content

Commit b56d8b1

Browse files
committed
DATAREDIS-481 - Allow customization of cache key conversion.
Allow ConversionService configuration via RedisCacheConfiguration. Accept cache keys that are either convertible to String or that override toString to obtain the String representation of the cache key.
1 parent 29796c7 commit b56d8b1

File tree

3 files changed

+141
-16
lines changed

3 files changed

+141
-16
lines changed

Diff for: src/main/java/org/springframework/data/redis/cache/RedisCache.java

+32-7
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,20 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18+
import java.lang.reflect.Method;
1819
import java.nio.ByteBuffer;
19-
import java.nio.charset.StandardCharsets;
2020
import java.util.concurrent.Callable;
2121

2222
import org.springframework.cache.support.AbstractValueAdaptingCache;
2323
import org.springframework.cache.support.NullValue;
2424
import org.springframework.cache.support.SimpleValueWrapper;
25-
import org.springframework.core.convert.support.ConfigurableConversionService;
25+
import org.springframework.core.convert.ConversionService;
26+
import org.springframework.core.convert.TypeDescriptor;
2627
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
2728
import org.springframework.data.redis.util.ByteUtils;
28-
import org.springframework.format.support.DefaultFormattingConversionService;
2929
import org.springframework.util.Assert;
3030
import org.springframework.util.ObjectUtils;
31+
import org.springframework.util.ReflectionUtils;
3132

3233
/**
3334
* {@link org.springframework.cache.Cache} implementation using for Redis as underlying store.
@@ -47,7 +48,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
4748
private final String name;
4849
private final RedisCacheWriter cacheWriter;
4950
private final RedisCacheConfiguration cacheConfig;
50-
private final ConfigurableConversionService conversionService = new DefaultFormattingConversionService();
51+
private final ConversionService conversionService;
5152

5253
/**
5354
* Create new {@link RedisCache}.
@@ -67,8 +68,7 @@ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfig
6768
this.name = name;
6869
this.cacheWriter = cacheWriter;
6970
this.cacheConfig = cacheConfig;
70-
71-
conversionService.addConverter(String.class, byte[].class, source -> source.getBytes(StandardCharsets.UTF_8));
71+
this.conversionService = cacheConfig.getConversionService();
7272
}
7373

7474
/*
@@ -259,14 +259,39 @@ protected Object deserializeCacheValue(byte[] value) {
259259
*/
260260
protected String createCacheKey(Object key) {
261261

262-
String convertedKey = conversionService.convert(key, String.class);
262+
String convertedKey = convertKey(key);
263+
263264
if (!cacheConfig.usePrefix()) {
264265
return convertedKey;
265266
}
266267

267268
return prefixCacheKey(convertedKey);
268269
}
269270

271+
/**
272+
* Convert {@code key} to a {@link String} representation used for cache key creation.
273+
*
274+
* @param key will never be {@literal null}.
275+
* @return never {@literal null}.
276+
* @throws IllegalStateException if {@code key} cannot be converted to {@link String}.
277+
*/
278+
protected String convertKey(Object key) {
279+
280+
TypeDescriptor source = TypeDescriptor.forObject(key);
281+
if (conversionService.canConvert(source, TypeDescriptor.valueOf(String.class))) {
282+
return conversionService.convert(key, String.class);
283+
}
284+
285+
Method toString = ReflectionUtils.findMethod(key.getClass(), "toString");
286+
287+
if (toString != null && !Object.class.equals(toString.getDeclaringClass())) {
288+
return key.toString();
289+
}
290+
291+
throw new IllegalStateException(
292+
"Cannot convert " + source + " to String. Register a Converter or override toString().");
293+
}
294+
270295
private byte[] createAndConvertCacheKey(Object key) {
271296
return serializeCacheKey(createCacheKey(key));
272297
}

Diff for: src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java

+62-9
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18+
import java.nio.charset.StandardCharsets;
1819
import java.time.Duration;
1920
import java.util.Optional;
2021

2122
import org.springframework.cache.Cache;
23+
import org.springframework.cache.interceptor.SimpleKey;
24+
import org.springframework.core.convert.ConversionService;
25+
import org.springframework.core.convert.converter.ConverterRegistry;
2226
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
2327
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
2428
import org.springframework.data.redis.serializer.StringRedisSerializer;
29+
import org.springframework.format.support.DefaultFormattingConversionService;
2530
import org.springframework.util.Assert;
2631

2732
/**
@@ -44,16 +49,20 @@ public class RedisCacheConfiguration {
4449
private final SerializationPair<String> keySerializationPair;
4550
private final SerializationPair<Object> valueSerializationPair;
4651

52+
private final ConversionService conversionService;
53+
4754
@SuppressWarnings("unchecked")
4855
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, String keyPrefix,
49-
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair) {
56+
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
57+
ConversionService conversionService) {
5058

5159
this.ttl = ttl;
5260
this.cacheNullValues = cacheNullValues;
5361
this.usePrefix = usePrefix;
5462
this.keyPrefix = keyPrefix;
5563
this.keySerializationPair = keySerializationPair;
5664
this.valueSerializationPair = (SerializationPair<Object>) valueSerializationPair;
65+
this.conversionService = conversionService;
5766
}
5867

5968
/**
@@ -71,15 +80,22 @@ private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean u
7180
* <dd>StringRedisSerializer.class</dd>
7281
* <dt>value serializer</dt>
7382
* <dd>JdkSerializationRedisSerializer.class</dd>
83+
* <dt>conversion service</dt>
84+
* <dd>{@link DefaultFormattingConversionService} with {@link #registerDefaultConverters(ConverterRegistry) default}
85+
* cache key converters</dd>
7486
* </dl>
7587
*
7688
* @return new {@link RedisCacheConfiguration}.
7789
*/
7890
public static RedisCacheConfiguration defaultCacheConfig() {
7991

92+
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
93+
94+
registerDefaultConverters(conversionService);
95+
8096
return new RedisCacheConfiguration(Duration.ZERO, true, true, null,
8197
SerializationPair.fromSerializer(new StringRedisSerializer()),
82-
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
98+
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()), conversionService);
8399
}
84100

85101
/**
@@ -93,7 +109,7 @@ public RedisCacheConfiguration entryTtl(Duration ttl) {
93109
Assert.notNull(ttl, "TTL duration must not be null!");
94110

95111
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
96-
valueSerializationPair);
112+
valueSerializationPair, conversionService);
97113
}
98114

99115
/**
@@ -106,8 +122,8 @@ public RedisCacheConfiguration prefixKeysWith(String prefix) {
106122

107123
Assert.notNull(prefix, "Prefix must not be null!");
108124

109-
return new RedisCacheConfiguration(ttl, cacheNullValues, true, prefix, keySerializationPair,
110-
valueSerializationPair);
125+
return new RedisCacheConfiguration(ttl, cacheNullValues, true, prefix, keySerializationPair, valueSerializationPair,
126+
conversionService);
111127
}
112128

113129
/**
@@ -119,7 +135,8 @@ public RedisCacheConfiguration prefixKeysWith(String prefix) {
119135
* @return new {@link RedisCacheConfiguration}.
120136
*/
121137
public RedisCacheConfiguration disableCachingNullValues() {
122-
return new RedisCacheConfiguration(ttl, false, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair);
138+
return new RedisCacheConfiguration(ttl, false, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair,
139+
conversionService);
123140
}
124141

125142
/**
@@ -132,7 +149,21 @@ public RedisCacheConfiguration disableCachingNullValues() {
132149
public RedisCacheConfiguration disableKeyPrefix() {
133150

134151
return new RedisCacheConfiguration(ttl, cacheNullValues, false, keyPrefix, keySerializationPair,
135-
valueSerializationPair);
152+
valueSerializationPair, conversionService);
153+
}
154+
155+
/**
156+
* Define the {@link ConversionService} used for cache key to {@link String} conversion.
157+
*
158+
* @param conversionService must not be {@literal null}.
159+
* @return new {@link RedisCacheConfiguration}.
160+
*/
161+
public RedisCacheConfiguration withConversionService(ConversionService conversionService) {
162+
163+
Assert.notNull(conversionService, "ConversionService must not be null!");
164+
165+
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
166+
valueSerializationPair, conversionService);
136167
}
137168

138169
/**
@@ -146,7 +177,7 @@ public RedisCacheConfiguration serializeKeysWith(SerializationPair<String> keySe
146177
Assert.notNull(keySerializationPair, "KeySerializationPair must not be null!");
147178

148179
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
149-
valueSerializationPair);
180+
valueSerializationPair, conversionService);
150181
}
151182

152183
/**
@@ -160,7 +191,7 @@ public RedisCacheConfiguration serializeValuesWith(SerializationPair<?> valueSer
160191
Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null!");
161192

162193
return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
163-
valueSerializationPair);
194+
valueSerializationPair, conversionService);
164195
}
165196

166197
/**
@@ -206,4 +237,26 @@ public Duration getTtl() {
206237
return ttl;
207238
}
208239

240+
/**
241+
* @return The {@link ConversionService} used for cache key to {@link String} conversion. Never {@literal null}.
242+
*/
243+
public ConversionService getConversionService() {
244+
return conversionService;
245+
}
246+
247+
/**
248+
* Registers default cache key converters. The following converters get registered:
249+
* <ul>
250+
* <li>{@link String} to {@link byte byte[]} using UTF-8 encoding.</li>
251+
* <li>{@link SimpleKey} to {@link String}</li>
252+
*
253+
* @param registry must not be {@literal null}.
254+
*/
255+
public static void registerDefaultConverters(ConverterRegistry registry) {
256+
257+
Assert.notNull(registry, "ConverterRegistry must not be null!");
258+
259+
registry.addConverter(String.class, byte[].class, source -> source.getBytes(StandardCharsets.UTF_8));
260+
registry.addConverter(SimpleKey.class, String.class, SimpleKey::toString);
261+
}
209262
}

Diff for: src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java

+47
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import lombok.AllArgsConstructor;
2121
import lombok.Data;
2222
import lombok.NoArgsConstructor;
23+
import lombok.RequiredArgsConstructor;
2324

2425
import java.io.Serializable;
2526
import java.nio.charset.Charset;
@@ -34,6 +35,7 @@
3435
import org.junit.runners.Parameterized;
3536
import org.junit.runners.Parameterized.Parameters;
3637
import org.springframework.cache.Cache.ValueWrapper;
38+
import org.springframework.cache.interceptor.SimpleKey;
3739
import org.springframework.cache.support.NullValue;
3840
import org.springframework.data.redis.ConnectionFactoryTracker;
3941
import org.springframework.data.redis.connection.RedisConnection;
@@ -47,6 +49,7 @@
4749
* {@link RedisConnectionFactory} pairs.
4850
*
4951
* @author Christoph Strobl
52+
* @author Mark Paluch
5053
*/
5154
@RunWith(Parameterized.class)
5255
public class RedisCacheTests {
@@ -177,6 +180,38 @@ public void getShouldRetrieveEntry() {
177180
assertThat(result.get()).isEqualTo(sample);
178181
}
179182

183+
@Test // DATAREDIS-481
184+
public void shouldReadAndWriteSimpleCacheKey() {
185+
186+
SimpleKey key = new SimpleKey("param-1", "param-2");
187+
188+
cache.put(key, sample);
189+
190+
ValueWrapper result = cache.get(key);
191+
assertThat(result).isNotNull();
192+
assertThat(result.get()).isEqualTo(sample);
193+
}
194+
195+
@Test(expected = IllegalStateException.class) // DATAREDIS-481
196+
public void shouldRejectNonInvalidKey() {
197+
198+
InvalidKey key = new InvalidKey(sample.getFirstame(), sample.getBirthdate());
199+
200+
cache.put(key, sample);
201+
}
202+
203+
@Test // DATAREDIS-481
204+
public void shouldAllowComplexKeyWithToStringMethod() {
205+
206+
ComplexKey key = new ComplexKey(sample.getFirstame(), sample.getBirthdate());
207+
208+
cache.put(key, sample);
209+
210+
ValueWrapper result = cache.get(key);
211+
assertThat(result).isNotNull();
212+
assertThat(result.get()).isEqualTo(sample);
213+
}
214+
180215
@Test // DATAREDIS-481
181216
public void getShouldReturnNullWhenKeyDoesNotExist() {
182217
assertThat(cache.get(key)).isNull();
@@ -251,4 +286,16 @@ static class Person implements Serializable {
251286
Date birthdate;
252287
}
253288

289+
@RequiredArgsConstructor // toString not overridden
290+
static class InvalidKey implements Serializable {
291+
final String firstame;
292+
final Date birthdate;
293+
}
294+
295+
@Data
296+
@RequiredArgsConstructor
297+
static class ComplexKey implements Serializable {
298+
final String firstame;
299+
final Date birthdate;
300+
}
254301
}

0 commit comments

Comments
 (0)