diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java index e6bcb0eb4..607b032ec 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -17,11 +17,17 @@ package org.springframework.data.redis.cache; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.StringJoiner; import java.util.concurrent.Callable; import org.springframework.cache.support.AbstractValueAdaptingCache; import org.springframework.cache.support.NullValue; import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.redis.serializer.RedisSerializer; @@ -33,14 +39,14 @@ import org.springframework.util.ReflectionUtils; /** * {@link org.springframework.cache.Cache} implementation using for Redis as underlying store. - *

+ *

* Use {@link RedisCacheManager} to create {@link RedisCache} instances. * * @author Christoph Strobl * @author Mark Paluch - * @since 2.0 * @see RedisCacheConfiguration * @see RedisCacheWriter + * @since 2.0 */ public class RedisCache extends AbstractValueAdaptingCache { @@ -281,8 +287,19 @@ public class RedisCache extends AbstractValueAdaptingCache { protected String convertKey(Object key) { TypeDescriptor source = TypeDescriptor.forObject(key); + if (conversionService.canConvert(source, TypeDescriptor.valueOf(String.class))) { - return conversionService.convert(key, String.class); + try { + return conversionService.convert(key, String.class); + } catch (ConversionFailedException e) { + + // may fail if the given key is a collection + if (isCollectionLikeOrMap(source)) { + return convertCollectionLikeOrMapKey(key, source); + } + + throw e; + } } Method toString = ReflectionUtils.findMethod(key.getClass(), "toString"); @@ -291,8 +308,39 @@ public class RedisCache extends AbstractValueAdaptingCache { return key.toString(); } - throw new IllegalStateException( - String.format("Cannot convert %s to String. Register a Converter or override toString().", source)); + throw new IllegalStateException(String.format( + "Cannot convert cache key %s to String. Please provide a suitable Converter via 'RedisCacheConfiguration.withConversionService(...)' or override '%s.toString()'.", + source, key != null ? key.getClass().getSimpleName() : "Object")); + } + + @Nullable + private String convertCollectionLikeOrMapKey(Object key, TypeDescriptor source) { + + if (source.isMap()) { + + String target = "{"; + for (Entry entry : ((Map) key).entrySet()) { + target += (convertKey(entry.getKey()) + "=" + convertKey(entry.getValue())); + } + target += "}"; + return target; + } else if (source.isCollection() || source.isArray()) { + + StringJoiner sj = new StringJoiner(","); + + Collection collection = source.isCollection() ? (Collection) key + : Arrays.asList(ObjectUtils.toObjectArray(key)); + + for (Object val : collection) { + sj.add(convertKey(val)); + } + return "[" + sj.toString() + "]"; + } + return null; + } + + private boolean isCollectionLikeOrMap(TypeDescriptor source) { + return source.isArray() || source.isCollection() || source.isMap(); } private byte[] createAndConvertCacheKey(Object key) { diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index 4a5c971ee..dcfdaa2ec 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor; import java.io.Serializable; import java.nio.charset.Charset; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.function.Consumer; @@ -36,11 +37,11 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.springframework.cache.Cache.ValueWrapper; import org.springframework.cache.interceptor.SimpleKey; +import org.springframework.cache.interceptor.SimpleKeyGenerator; import org.springframework.cache.support.NullValue; import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; import org.springframework.data.redis.serializer.RedisSerializer; @@ -300,6 +301,58 @@ public class RedisCacheTests { assertThat(result.get()).isEqualTo(sample); } + @Test // DATAREDIS-1032 + public void cacheShouldAllowListKeyCacheKeysOfSimpleTypes() { + + Object key = SimpleKeyGenerator.generateKey(Collections.singletonList("my-cache-key-in-a-list")); + cache.put(key, sample); + + Object target = cache.get(SimpleKeyGenerator.generateKey(Collections.singletonList("my-cache-key-in-a-list"))); + assertThat(((ValueWrapper) target).get()).isEqualTo(sample); + } + + @Test // DATAREDIS-1032 + public void cacheShouldAllowArrayKeyCacheKeysOfSimpleTypes() { + + Object key = SimpleKeyGenerator.generateKey(new String[] { "my-cache-key-in-an-array" }); + cache.put(key, sample); + + Object target = cache.get(SimpleKeyGenerator.generateKey(new String[] { "my-cache-key-in-an-array" })); + assertThat(((ValueWrapper) target).get()).isEqualTo(sample); + } + + @Test // DATAREDIS-1032 + public void cacheShouldAllowListCacheKeysOfComplexTypes() { + + Object key = SimpleKeyGenerator + .generateKey(Collections.singletonList(new ComplexKey(sample.getFirstame(), sample.getBirthdate()))); + cache.put(key, sample); + + Object target = cache.get(SimpleKeyGenerator + .generateKey(Collections.singletonList(new ComplexKey(sample.getFirstame(), sample.getBirthdate())))); + assertThat(((ValueWrapper) target).get()).isEqualTo(sample); + } + + @Test // DATAREDIS-1032 + public void cacheShouldAllowMapCacheKeys() { + + Object key = SimpleKeyGenerator + .generateKey(Collections.singletonMap("map-key", new ComplexKey(sample.getFirstame(), sample.getBirthdate()))); + cache.put(key, sample); + + Object target = cache.get(SimpleKeyGenerator + .generateKey(Collections.singletonMap("map-key", new ComplexKey(sample.getFirstame(), sample.getBirthdate())))); + assertThat(((ValueWrapper) target).get()).isEqualTo(sample); + } + + @Test // DATAREDIS-1032 + public void cacheShouldFailOnNonConvertibleCacheKey() { + + Object key = SimpleKeyGenerator + .generateKey(Collections.singletonList(new InvalidKey(sample.getFirstame(), sample.getBirthdate()))); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> cache.put(key, sample)); + } + void doWithConnection(Consumer callback) { RedisConnection connection = connectionFactory.getConnection(); try {