diff --git a/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java b/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java index ff8664a06..4947350b3 100644 --- a/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java +++ b/src/main/java/org/springframework/data/redis/cache/CacheKeyPrefix.java @@ -18,11 +18,12 @@ package org.springframework.data.redis.cache; import org.springframework.util.Assert; /** - * {@link CacheKeyPrefix} provides a hook for creating custom prefixes prepended to the actual {@literal key} stored in - * Redis. + * {@link CacheKeyPrefix} is a callback hook for creating custom prefixes prepended to the actual {@literal key} + * stored in Redis. * * @author Christoph Strobl * @author Mark Paluch + * @author John Blum * @since 2.0.4 */ @FunctionalInterface @@ -36,16 +37,20 @@ public interface CacheKeyPrefix { String SEPARATOR = "::"; /** - * Compute the prefix for the actual {@literal key} stored in Redis. + * Compute the {@link String prefix} for the actual {@literal cache key} stored in Redis. * - * @param cacheName will never be {@literal null}. - * @return never {@literal null}. + * @param cacheName {@link String name} of the cache in which the key is stored; + * will never be {@literal null}. + * @return the computed {@link String prefix} of the {@literal cache key} stored in Redis; + * never {@literal null}. */ String compute(String cacheName); /** - * Creates a default {@link CacheKeyPrefix} scheme that prefixes cache keys with {@code cacheName} followed by double - * colons. A cache named {@code myCache} will prefix all cache keys with {@code myCache::}. + * Creates a default {@link CacheKeyPrefix} scheme that prefixes cache keys with the {@link String name} + * of the cache followed by double colons. + * + * For example, a cache named {@literal myCache} will prefix all cache keys with {@literal myCache::}. * * @return the default {@link CacheKeyPrefix} scheme. */ @@ -54,9 +59,12 @@ public interface CacheKeyPrefix { } /** - * Creates a {@link CacheKeyPrefix} scheme that prefixes cache keys with the given {@code prefix}. The prefix is - * prepended to the {@code cacheName} followed by double colons. A prefix {@code redis-} with a cache named - * {@code myCache} results in {@code redis-myCache::}. + * Creates a {@link CacheKeyPrefix} scheme that prefixes cache keys with the given {@link String prefix}. + * + * The {@link String prefix} is prepended to the {@link String cacheName} followed by double colons. + * + * For example, a prefix {@literal redis-} with a cache named {@literal myCache} + * results in {@literal redis-myCache::}. * * @param prefix must not be {@literal null}. * @return the default {@link CacheKeyPrefix} scheme. @@ -65,6 +73,7 @@ public interface CacheKeyPrefix { static CacheKeyPrefix prefixed(String prefix) { Assert.notNull(prefix, "Prefix must not be null"); + return name -> prefix + name + SEPARATOR; } } 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 c4cbeac97..f6877da44 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -24,12 +24,14 @@ import java.util.Map.Entry; import java.util.StringJoiner; import java.util.concurrent.Callable; +import org.springframework.cache.Cache; 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.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.util.ByteUtils; import org.springframework.lang.Nullable; @@ -38,63 +40,73 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; /** - * {@link org.springframework.cache.Cache} implementation using for Redis as underlying store. - *

+ * {@link org.springframework.cache.Cache} implementation using for Redis as the underlying store for cache data. + * * Use {@link RedisCacheManager} to create {@link RedisCache} instances. * * @author Christoph Strobl * @author Mark Paluch * @author Piotr Mionskowski * @author Jos Roseboom - * @see RedisCacheConfiguration - * @see RedisCacheWriter + * @author John Blum + * @see org.springframework.cache.support.AbstractValueAdaptingCache * @since 2.0 */ public class RedisCache extends AbstractValueAdaptingCache { private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); - private final String name; + private final RedisCacheConfiguration cacheConfiguration; + private final RedisCacheWriter cacheWriter; - private final RedisCacheConfiguration cacheConfig; - private final ConversionService conversionService; + + private final String name; /** - * Create new {@link RedisCache}. + * Create a new {@link RedisCache}. * - * @param name must not be {@literal null}. - * @param cacheWriter must not be {@literal null}. - * @param cacheConfig must not be {@literal null}. + * @param name {@link String name} for this {@link Cache}; must not be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache on creation; + * must not be {@literal null}. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null} or the given {@link String} name for this {@link RedisCache} is {@literal null}. */ - protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) { + protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfiguration) { - super(cacheConfig.getAllowCacheNullValues()); + super(cacheConfiguration.getAllowCacheNullValues()); Assert.notNull(name, "Name must not be null"); Assert.notNull(cacheWriter, "CacheWriter must not be null"); - Assert.notNull(cacheConfig, "CacheConfig must not be null"); + Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null"); this.name = name; this.cacheWriter = cacheWriter; - this.cacheConfig = cacheConfig; - this.conversionService = cacheConfig.getConversionService(); + this.cacheConfiguration = cacheConfiguration; } - @Override - protected Object lookup(Object key) { - byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key)); + /** + * Get {@link RedisCacheConfiguration} used. + * + * @return immutable {@link RedisCacheConfiguration}. Never {@literal null}. + */ + public RedisCacheConfiguration getCacheConfiguration() { + return this.cacheConfiguration; + } - if (value == null) { - return null; - } + protected RedisCacheWriter getCacheWriter() { + return this.cacheWriter; + } - return deserializeCacheValue(value); + protected ConversionService getConversionService() { + return getCacheConfiguration().getConversionService(); } @Override public String getName() { - return name; + return this.name; } @Override @@ -102,38 +114,59 @@ public class RedisCache extends AbstractValueAdaptingCache { return this.cacheWriter; } + /** + * Return the {@link CacheStatistics} snapshot for this cache instance. Statistics are accumulated per cache instance + * and not from the backing Redis data store. + * + * @return statistics object for this {@link RedisCache}. + * @since 2.4 + */ + public CacheStatistics getStatistics() { + return getCacheWriter().getCacheStatistics(getName()); + } + @Override @SuppressWarnings("unchecked") public T get(Object key, Callable valueLoader) { ValueWrapper result = get(key); - if (result != null) { - return (T) result.get(); - } - - return getSynchronized(key, valueLoader); + return result != null ? (T) result.get() + : getSynchronized(key, valueLoader); } @SuppressWarnings("unchecked") - private synchronized T getSynchronized(Object key, Callable valueLoader) { + private synchronized @Nullable T getSynchronized(Object key, Callable valueLoader) { ValueWrapper result = get(key); - if (result != null) { - return (T) result.get(); - } + return result != null ? (T) result.get() + : loadCacheValue(key, valueLoader); + } + + protected T loadCacheValue(Object key, Callable valueLoader) { T value; + try { value = valueLoader.call(); - } catch (Exception e) { - throw new ValueRetrievalException(key, valueLoader, e); + } catch (Exception cause) { + throw new ValueRetrievalException(key, valueLoader, cause); } + put(key, value); + return value; } + @Override + protected Object lookup(Object key) { + + byte[] value = getCacheWriter().get(getName(), createAndConvertCacheKey(key)); + + return value != null ? deserializeCacheValue(value) : null; + } + @Override public void put(Object key, @Nullable Object value) { @@ -141,12 +174,16 @@ public class RedisCache extends AbstractValueAdaptingCache { if (!isAllowNullValues() && cacheValue == null) { - throw new IllegalArgumentException(String.format( - "Cache '%s' does not allow 'null' values; Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration", - name)); + String message = String.format("Cache '%s' does not allow 'null' values; Avoid storing null" + + " via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null'" + + " via RedisCacheConfiguration", + getName()); + + throw new IllegalArgumentException(message); } - cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl()); + getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue), + getCacheConfiguration().getTtl()); } @Override @@ -158,19 +195,10 @@ public class RedisCache extends AbstractValueAdaptingCache { return get(key); } - byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), - cacheConfig.getTtl()); + byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key), + serializeCacheValue(cacheValue), getCacheConfiguration().getTtl()); - if (result == null) { - return null; - } - - return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))); - } - - @Override - public void evict(Object key) { - cacheWriter.remove(name, createAndConvertCacheKey(key)); + return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null; } @Override @@ -179,26 +207,15 @@ public class RedisCache extends AbstractValueAdaptingCache { } /** - * Clear keys that match the provided {@code keyPattern}. + * Clear keys that match the provided {@link String keyPattern}. *

* Useful when cache keys are formatted in a style where Redis patterns can be used for matching these. * - * @param keyPattern the pattern of the key + * @param keyPattern {@link String pattern} used to match Redis keys to clear. * @since 3.0 */ public void clear(String keyPattern) { - cacheWriter.clean(name, createAndConvertCacheKey(keyPattern)); - } - - /** - * Return the {@link CacheStatistics} snapshot for this cache instance. Statistics are accumulated per cache instance - * and not from the backing Redis data store. - * - * @return statistics object for this {@link RedisCache}. - * @since 2.4 - */ - public CacheStatistics getStatistics() { - return cacheWriter.getCacheStatistics(getName()); + getCacheWriter().clean(getName(), createAndConvertCacheKey(keyPattern)); } /** @@ -207,16 +224,12 @@ public class RedisCache extends AbstractValueAdaptingCache { * @since 2.4 */ public void clearStatistics() { - cacheWriter.clearStatistics(getName()); + getCacheWriter().clearStatistics(getName()); } - /** - * Get {@link RedisCacheConfiguration} used. - * - * @return immutable {@link RedisCacheConfiguration}. Never {@literal null}. - */ - public RedisCacheConfiguration getCacheConfiguration() { - return cacheConfig; + @Override + public void evict(Object key) { + getCacheWriter().remove(getName(), createAndConvertCacheKey(key)); } /** @@ -229,28 +242,28 @@ public class RedisCache extends AbstractValueAdaptingCache { @Nullable protected Object preProcessCacheValue(@Nullable Object value) { - if (value != null) { - return value; - } - - return isAllowNullValues() ? NullValue.INSTANCE : null; + return value != null ? value + : isAllowNullValues() ? NullValue.INSTANCE + : null; } /** - * Serialize the key. + * Serialize the given {@link String cache key}. * - * @param cacheKey must not be {@literal null}. - * @return never {@literal null}. + * @param cacheKey {@link String cache key} to serialize; must not be {@literal null}. + * @return an array of bytes from the given, serialized {@link String cache key}; never {@literal null}. + * @see RedisCacheConfiguration#getKeySerializationPair() */ protected byte[] serializeCacheKey(String cacheKey) { - return ByteUtils.getBytes(cacheConfig.getKeySerializationPair().write(cacheKey)); + return ByteUtils.getBytes(getCacheConfiguration().getKeySerializationPair().write(cacheKey)); } /** - * Serialize the value to cache. + * Serialize the {@link Object value} to cache as an array of bytes. * - * @param value must not be {@literal null}. - * @return never {@literal null}. + * @param value {@link Object} to serialize and cache; must not be {@literal null}. + * @return an array of bytes from the serialized {@link Object value}; never {@literal null}. + * @see RedisCacheConfiguration#getValueSerializationPair() */ protected byte[] serializeCacheValue(Object value) { @@ -258,14 +271,16 @@ public class RedisCache extends AbstractValueAdaptingCache { return BINARY_NULL_VALUE; } - return ByteUtils.getBytes(cacheConfig.getValueSerializationPair().write(value)); + return ByteUtils.getBytes(getCacheConfiguration().getValueSerializationPair().write(value)); } /** - * Deserialize the given value to the actual cache value. + * Deserialize the given the array of bytes to the actual {@link Object cache value}. * - * @param value must not be {@literal null}. - * @return can be {@literal null}. + * @param value array of bytes to deserialize; must not be {@literal null}. + * @return an {@link Object} deserialized from the array of bytes using the configured value + * {@link RedisSerializationContext.SerializationPair}; can be {@literal null}. + * @see RedisCacheConfiguration#getValueSerializationPair() */ @Nullable protected Object deserializeCacheValue(byte[] value) { @@ -274,7 +289,7 @@ public class RedisCache extends AbstractValueAdaptingCache { return NullValue.INSTANCE; } - return cacheConfig.getValueSerializationPair().read(ByteBuffer.wrap(value)); + return getCacheConfiguration().getValueSerializationPair().read(ByteBuffer.wrap(value)); } /** @@ -287,7 +302,7 @@ public class RedisCache extends AbstractValueAdaptingCache { String convertedKey = convertKey(key); - return cacheConfig.usePrefix() ? prefixCacheKey(convertedKey) : convertedKey; + return getCacheConfiguration().usePrefix() ? prefixCacheKey(convertedKey) : convertedKey; } /** @@ -305,61 +320,79 @@ public class RedisCache extends AbstractValueAdaptingCache { TypeDescriptor source = TypeDescriptor.forObject(key); + ConversionService conversionService = getConversionService(); + if (conversionService.canConvert(source, TypeDescriptor.valueOf(String.class))) { try { return conversionService.convert(key, String.class); - } catch (ConversionFailedException e) { + } catch (ConversionFailedException cause) { - // may fail if the given key is a collection + // May fail if the given key is a collection if (isCollectionLikeOrMap(source)) { return convertCollectionLikeOrMapKey(key, source); } - throw e; + throw cause; } } - Method toString = ReflectionUtils.findMethod(key.getClass(), "toString"); - - if (toString != null && !Object.class.equals(toString.getDeclaringClass())) { + if (hasToStringMethod(key)) { return key.toString(); } - throw new IllegalStateException(String.format( - "Cannot convert cache key %s to String; Please register a suitable Converter via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'", - source, key.getClass().getSimpleName())); + String message = String.format("Cannot convert cache key %s to String; Please register a suitable Converter" + + " via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'", + source, key.getClass().getName()); + + throw new IllegalStateException(message); + } + + private boolean hasToStringMethod(Object target) { + return hasToStringMethod(target.getClass()); + } + + private boolean hasToStringMethod(Class type) { + + Method toString = ReflectionUtils.findMethod(type, "toString"); + + return toString != null && !Object.class.equals(toString.getDeclaringClass()); + } + + private boolean isCollectionLikeOrMap(TypeDescriptor source) { + return source.isArray() || source.isCollection() || source.isMap(); } private String convertCollectionLikeOrMapKey(Object key, TypeDescriptor source) { if (source.isMap()) { + int count = 0; + StringBuilder target = new StringBuilder("{"); for (Entry entry : ((Map) key).entrySet()) { target.append(convertKey(entry.getKey())).append("=").append(convertKey(entry.getValue())); + target.append(++count > 1 ? ", " : ""); } + target.append("}"); return target.toString(); } else if (source.isCollection() || source.isArray()) { - StringJoiner sj = new StringJoiner(","); + StringJoiner stringJoiner = new StringJoiner(","); Collection collection = source.isCollection() ? (Collection) key : Arrays.asList(ObjectUtils.toObjectArray(key)); - for (Object val : collection) { - sj.add(convertKey(val)); + for (Object collectedKey : collection) { + stringJoiner.add(convertKey(collectedKey)); } - return "[" + sj.toString() + "]"; + + return "[" + stringJoiner + "]"; } - throw new IllegalArgumentException(String.format("Cannot convert cache key %s to String", key)); - } - - private boolean isCollectionLikeOrMap(TypeDescriptor source) { - return source.isArray() || source.isCollection() || source.isMap(); + throw new IllegalArgumentException(String.format("Cannot convert cache key [%s] to String", key)); } private byte[] createAndConvertCacheKey(Object key) { @@ -367,9 +400,7 @@ public class RedisCache extends AbstractValueAdaptingCache { } private String prefixCacheKey(String key) { - // allow contextual cache names by computing the key prefix on every call. - return cacheConfig.getKeyPrefixFor(name) + key; + return getCacheConfiguration().getKeyPrefixFor(getName()) + key; } - } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java index 582b15aba..74c73cc28 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java @@ -31,40 +31,23 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Immutable {@link RedisCacheConfiguration} helps customizing {@link RedisCache} behaviour such as caching - * {@literal null} values, cache key prefixes and binary serialization.
- * Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour from there - * on. + * Immutable {@link RedisCacheConfiguration} used to customize {@link RedisCache} behaviour, such as caching + * {@literal null} values, computing cache key prefixes and handling binary serialization. + * + * Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour + * from that point on. * * @author Christoph Strobl * @author Mark Paluch + * @author John Blum * @since 2.0 */ public class RedisCacheConfiguration { - private final Duration ttl; - private final boolean cacheNullValues; - private final CacheKeyPrefix keyPrefix; - private final boolean usePrefix; - - private final SerializationPair keySerializationPair; - private final SerializationPair valueSerializationPair; - - private final ConversionService conversionService; - - @SuppressWarnings("unchecked") - private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, - SerializationPair keySerializationPair, SerializationPair valueSerializationPair, - ConversionService conversionService) { - - this.ttl = ttl; - this.cacheNullValues = cacheNullValues; - this.usePrefix = usePrefix; - this.keyPrefix = keyPrefix; - this.keySerializationPair = keySerializationPair; - this.valueSerializationPair = (SerializationPair) valueSerializationPair; - this.conversionService = conversionService; - } + protected static final boolean DEFAULT_CACHE_NULL_VALUES = true; + protected static final boolean DEFAULT_USE_PREFIX = true; + protected static final boolean DO_NOT_CACHE_NULL_VALUES = false; + protected static final boolean DO_NOT_USE_PREFIX = false; /** * Default {@link RedisCacheConfiguration} using the following: @@ -123,23 +106,36 @@ public class RedisCacheConfiguration { registerDefaultConverters(conversionService); - return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), + return new RedisCacheConfiguration(Duration.ZERO, DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX, + CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService); } - /** - * Set the ttl to apply for cache entries. Use {@link Duration#ZERO} to declare an eternal cache. - * - * @param ttl must not be {@literal null}. - * @return new {@link RedisCacheConfiguration}. - */ - public RedisCacheConfiguration entryTtl(Duration ttl) { + private final boolean cacheNullValues; + private final boolean usePrefix; - Assert.notNull(ttl, "TTL duration must not be null"); + private final CacheKeyPrefix keyPrefix; - return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + private final ConversionService conversionService; + + private final Duration ttl; + + private final SerializationPair keySerializationPair; + private final SerializationPair valueSerializationPair; + + @SuppressWarnings("unchecked") + private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, + SerializationPair keySerializationPair, SerializationPair valueSerializationPair, + ConversionService conversionService) { + + this.ttl = ttl; + this.cacheNullValues = cacheNullValues; + this.usePrefix = usePrefix; + this.keyPrefix = keyPrefix; + this.keySerializationPair = keySerializationPair; + this.valueSerializationPair = (SerializationPair) valueSerializationPair; + this.conversionService = conversionService; } /** @@ -169,8 +165,8 @@ public class RedisCacheConfiguration { Assert.notNull(cacheKeyPrefix, "Function for computing prefix must not be null"); - return new RedisCacheConfiguration(ttl, cacheNullValues, true, cacheKeyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + return new RedisCacheConfiguration(ttl, cacheNullValues, DEFAULT_USE_PREFIX, cacheKeyPrefix, + keySerializationPair, valueSerializationPair, conversionService); } /** @@ -182,8 +178,8 @@ public class RedisCacheConfiguration { * @return new {@link RedisCacheConfiguration}. */ public RedisCacheConfiguration disableCachingNullValues() { - return new RedisCacheConfiguration(ttl, false, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair, - conversionService); + return new RedisCacheConfiguration(ttl, DO_NOT_CACHE_NULL_VALUES, usePrefix, keyPrefix, keySerializationPair, + valueSerializationPair, conversionService); } /** @@ -195,22 +191,22 @@ public class RedisCacheConfiguration { */ public RedisCacheConfiguration disableKeyPrefix() { - return new RedisCacheConfiguration(ttl, cacheNullValues, false, keyPrefix, keySerializationPair, + return new RedisCacheConfiguration(ttl, cacheNullValues, DO_NOT_USE_PREFIX, keyPrefix, keySerializationPair, valueSerializationPair, conversionService); } /** - * Define the {@link ConversionService} used for cache key to {@link String} conversion. + * Set the ttl to apply for cache entries. Use {@link Duration#ZERO} to declare an eternal cache. * - * @param conversionService must not be {@literal null}. + * @param ttl must not be {@literal null}. * @return new {@link RedisCacheConfiguration}. */ - public RedisCacheConfiguration withConversionService(ConversionService conversionService) { + public RedisCacheConfiguration entryTtl(Duration ttl) { - Assert.notNull(conversionService, "ConversionService must not be null"); + Assert.notNull(ttl, "TTL duration must not be null"); return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + valueSerializationPair, conversionService); } /** @@ -242,16 +238,24 @@ public class RedisCacheConfiguration { } /** - * Get the computed {@literal key} prefix for a given {@literal cacheName}. + * Define the {@link ConversionService} used for cache key to {@link String} conversion. * - * @return never {@literal null}. - * @since 2.0.4 + * @param conversionService must not be {@literal null}. + * @return new {@link RedisCacheConfiguration}. */ - public String getKeyPrefixFor(String cacheName) { + public RedisCacheConfiguration withConversionService(ConversionService conversionService) { - Assert.notNull(cacheName, "Cache name must not be null"); + Assert.notNull(conversionService, "ConversionService must not be null"); - return keyPrefix.compute(cacheName); + return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, + valueSerializationPair, conversionService); + } + + /** + * @return {@literal true} if caching {@literal null} is allowed. + */ + public boolean getAllowCacheNullValues() { + return cacheNullValues; } /** @@ -263,10 +267,23 @@ public class RedisCacheConfiguration { } /** - * @return {@literal true} if caching {@literal null} is allowed. + * @return The {@link ConversionService} used for cache key to {@link String} conversion. Never {@literal null}. */ - public boolean getAllowCacheNullValues() { - return cacheNullValues; + public ConversionService getConversionService() { + return conversionService; + } + + /** + * Get the computed {@literal key} prefix for a given {@literal cacheName}. + * + * @return never {@literal null}. + * @since 2.0.4 + */ + public String getKeyPrefixFor(String cacheName) { + + Assert.notNull(cacheName, "Cache name must not be null"); + + return keyPrefix.compute(cacheName); } /** @@ -291,18 +308,12 @@ public class RedisCacheConfiguration { } /** - * @return The {@link ConversionService} used for cache key to {@link String} conversion. Never {@literal null}. - */ - public ConversionService getConversionService() { - return conversionService; - } - - /** - * Add a {@link Converter} for extracting the {@link String} representation of a cache key if no suitable - * {@link Object#toString()} method is present. + * Adds a {@link Converter} to extract the {@link String} representation of a {@literal cache key} + * if no suitable {@link Object#toString()} method is present. * - * @param cacheKeyConverter - * @throws IllegalStateException if {@link #getConversionService()} does not allow converter registration. + * @param cacheKeyConverter {@link Converter} used to convert a {@literal cache key} into a {@link String}. + * @throws IllegalStateException if {@link #getConversionService()} does not allow {@link Converter} registration. + * @see org.springframework.core.convert.converter.Converter * @since 2.2 */ public void addCacheKeyConverter(Converter cacheKeyConverter) { @@ -310,32 +321,40 @@ public class RedisCacheConfiguration { } /** - * Configure the underlying conversion system used to extract the cache key. + * Configure the underlying {@link ConversionService} used to extract the {@literal cache key}. * - * @param registryConsumer never {@literal null}. - * @throws IllegalStateException if {@link #getConversionService()} does not allow converter registration. + * @param registryConsumer {@link Consumer} used to register a {@link Converter} + * with the configured {@link ConverterRegistry}; never {@literal null}. + * @throws IllegalStateException if {@link #getConversionService()} does not allow {@link Converter} registration. + * @see org.springframework.core.convert.converter.ConverterRegistry * @since 2.2 */ public void configureKeyConverters(Consumer registryConsumer) { if (!(getConversionService() instanceof ConverterRegistry)) { - throw new IllegalStateException(String.format( - "'%s' returned by getConversionService() does not allow converter registration;" // - + " Please make sure to provide a ConversionService that implements ConverterRegistry", - getConversionService().getClass().getSimpleName())); + + String message = "'%s' returned by getConversionService() does not allow Converter registration;" + + " Please make sure to provide a ConversionService that implements ConverterRegistry"; + + throw new IllegalStateException(String.format(message, getConversionService().getClass().getName())); } registryConsumer.accept((ConverterRegistry) getConversionService()); } /** - * Registers default cache key converters. The following converters get registered: + * Registers default cache {@link Converter key converters}. + * + * The following converters get registered: + * *
    *
  • {@link String} to {@link byte byte[]} using UTF-8 encoding.
  • *
  • {@link SimpleKey} to {@link String}
  • *
* - * @param registry must not be {@literal null}. + * @param registry {@link ConverterRegistry} in which the {@link Converter key converters} are registered; + * must not be {@literal null}. + * @see org.springframework.core.convert.converter.ConverterRegistry */ public static void registerDefaultConverters(ConverterRegistry registry) { diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java index 18f26a494..d1a8a805b 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java @@ -19,154 +19,90 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.util.RedisAssertions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * {@link org.springframework.cache.CacheManager} backed by a {@link RedisCache Redis} cache. + * {@link CacheManager} backed by a {@link RedisCache}. *

- * This cache manager creates caches by default upon first write. Empty caches are not visible on Redis due to how Redis - * represents empty data structures. + * This {@link CacheManager} creates {@link Cache caches} by default upon first write. Empty {@link Cache caches} + * are not visible in Redis due to how Redis represents empty data structures. *

- * Caches requiring a different {@link RedisCacheConfiguration} than the default configuration can be specified via - * {@link RedisCacheManagerBuilder#withInitialCacheConfigurations(Map)}. + * {@link Cache Caches} requiring a different {@link RedisCacheConfiguration} than the default cache configuration + * can be specified via {@link RedisCacheManagerBuilder#withInitialCacheConfigurations(Map)} or individually + * using {@link RedisCacheManagerBuilder#withCacheConfiguration(String, RedisCacheConfiguration)}. * * @author Christoph Strobl * @author Mark Paluch * @author Yanming Zhou + * @author John Blum + * @see org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0 - * @see RedisCacheConfiguration - * @see RedisCacheWriter */ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager { - private final RedisCacheWriter cacheWriter; - private final RedisCacheConfiguration defaultCacheConfig; - private final Map initialCacheConfiguration; - private final boolean allowInFlightCacheCreation; + protected static final boolean DEFAULT_ALLOW_RUNTIME_CACHE_CREATION = true; /** - * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}. + * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager}. * - * @param cacheWriter must not be {@literal null}. - * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use - * {@link RedisCacheConfiguration#defaultCacheConfig()}. - * @param allowInFlightCacheCreation allow create unconfigured caches. - * @since 2.0.4 + * @return new {@link RedisCacheManagerBuilder}. + * @since 2.3 */ - private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, - boolean allowInFlightCacheCreation) { + public static RedisCacheManagerBuilder builder() { + return new RedisCacheManagerBuilder(); + } + + /** + * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} + * using the given {@link RedisCacheWriter}. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing + * appropriate Redis commands; must not be {@literal null}. + * @return new {@link RedisCacheManagerBuilder}. + * @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheWriter + */ + public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) { Assert.notNull(cacheWriter, "CacheWriter must not be null"); - Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null"); - this.cacheWriter = cacheWriter; - this.defaultCacheConfig = defaultCacheConfiguration; - this.initialCacheConfiguration = new LinkedHashMap<>(); - this.allowInFlightCacheCreation = allowInFlightCacheCreation; + return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter); } /** - * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}. + * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} + * using the given {@link RedisConnectionFactory}. * - * @param cacheWriter must not be {@literal null}. - * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use - * {@link RedisCacheConfiguration#defaultCacheConfig()}. + * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} + * to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @return new {@link RedisCacheManagerBuilder}. + * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. + * @see org.springframework.data.redis.connection.RedisConnectionFactory */ - public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { - this(cacheWriter, defaultCacheConfiguration, true); + public static RedisCacheManagerBuilder builder(RedisConnectionFactory connectionFactory) { + + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + + return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory); } /** - * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}. + * Factory method used to construct a new {@link RedisCacheManager} using the given {@link RedisConnectionFactory} + * with caching defaults applied. * - * @param cacheWriter must not be {@literal null}. - * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use - * {@link RedisCacheConfiguration#defaultCacheConfig()}. - * @param initialCacheNames optional set of known cache names that will be created with given - * {@literal defaultCacheConfiguration}. - */ - public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, - String... initialCacheNames) { - - this(cacheWriter, defaultCacheConfiguration, true, initialCacheNames); - } - - /** - * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}. - * - * @param cacheWriter must not be {@literal null}. - * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use - * {@link RedisCacheConfiguration#defaultCacheConfig()}. - * @param allowInFlightCacheCreation if set to {@literal true} no new caches can be acquire at runtime but limited to - * the given list of initial cache names. - * @param initialCacheNames optional set of known cache names that will be created with given - * {@literal defaultCacheConfiguration}. - * @since 2.0.4 - */ - public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, - boolean allowInFlightCacheCreation, String... initialCacheNames) { - - this(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation); - - for (String cacheName : initialCacheNames) { - this.initialCacheConfiguration.put(cacheName, defaultCacheConfiguration); - } - } - - /** - * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}. - * - * @param cacheWriter must not be {@literal null}. - * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use - * {@link RedisCacheConfiguration#defaultCacheConfig()}. - * @param initialCacheConfigurations Map of known cache names along with the configuration to use for those caches. - * Must not be {@literal null}. - */ - public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, - Map initialCacheConfigurations) { - - this(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, true); - } - - /** - * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}. - * - * @param cacheWriter must not be {@literal null}. - * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use - * {@link RedisCacheConfiguration#defaultCacheConfig()}. - * @param initialCacheConfigurations Map of known cache names along with the configuration to use for those caches. - * Must not be {@literal null}. - * @param allowInFlightCacheCreation if set to {@literal false} this cache manager is limited to the initial cache - * configurations and will not create new caches at runtime. - * @since 2.0.4 - */ - public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, - Map initialCacheConfigurations, boolean allowInFlightCacheCreation) { - - this(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation); - - Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null"); - - this.initialCacheConfiguration.putAll(initialCacheConfigurations); - } - - /** - * Create a new {@link RedisCacheManager} with defaults applied. *

*
locking
*
disabled
@@ -182,113 +118,330 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager *
enabled
*
* - * @param connectionFactory must not be {@literal null}. - * @return new instance of {@link RedisCacheManager}. + * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} + * to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @return new {@link RedisCacheManager}. + * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. + * @see org.springframework.data.redis.connection.RedisConnectionFactory */ public static RedisCacheManager create(RedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), - RedisCacheConfiguration.defaultCacheConfig()); + RedisCacheConfiguration.defaultCacheConfig()); + } + + private final boolean allowRuntimeCacheCreation; + + private final RedisCacheConfiguration defaultCacheConfiguration; + + private final RedisCacheWriter cacheWriter; + + private final Map initialCacheConfiguration; + + /** + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} + * and a default {@link RedisCacheConfiguration}. + * + * Allows cache creation at runtime. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} + * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter + */ + public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { + this(cacheWriter, defaultCacheConfiguration, DEFAULT_ALLOW_RUNTIME_CACHE_CREATION); } /** - * Entry point for builder style {@link RedisCacheManager} configuration. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} + * and a default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. * - * @return new {@link RedisCacheManagerBuilder}. - * @since 2.3 + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} + * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter + * @since 2.0.4 */ - public static RedisCacheManagerBuilder builder() { - return new RedisCacheManagerBuilder(); + private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + boolean allowRuntimeCacheCreation) { + + this.defaultCacheConfiguration = RedisAssertions.requireObject(defaultCacheConfiguration, + "DefaultCacheConfiguration must not be null"); + + this.cacheWriter = RedisAssertions.requireObject(cacheWriter, "CacheWriter must not be null"); + this.initialCacheConfiguration = new LinkedHashMap<>(); + this.allowRuntimeCacheCreation = allowRuntimeCacheCreation; } /** - * Entry point for builder style {@link RedisCacheManager} configuration. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} + * and a default {@link RedisCacheConfiguration}, along with an optional, initial set of {@link String cache names} + * used to create {@link RedisCache Redis caches} on startup. * - * @param connectionFactory must not be {@literal null}. - * @return new {@link RedisCacheManagerBuilder}. + * Allows cache creation at runtime. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} + * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param initialCacheNames optional set of {@link String cache names} used to create {@link RedisCache Redis caches} + * on startup. The default {@link RedisCacheConfiguration} will be applied to each cache. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter */ - public static RedisCacheManagerBuilder builder(RedisConnectionFactory connectionFactory) { + public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + String... initialCacheNames) { - Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - - return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory); + this(cacheWriter, defaultCacheConfiguration, DEFAULT_ALLOW_RUNTIME_CACHE_CREATION, initialCacheNames); } /** - * Entry point for builder style {@link RedisCacheManager} configuration. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} + * and default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. * - * @param cacheWriter must not be {@literal null}. - * @return new {@link RedisCacheManagerBuilder}. + * Additionally, the optional, initial set of {@link String cache names} witll be used to create + * {@link RedisCache Redis caches} on startup. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} + * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * @param initialCacheNames optional set of {@link String cache names} used to create {@link RedisCache Redis caches} + * on startup. The default {@link RedisCacheConfiguration} will be applied to each cache. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter + * @since 2.0.4 */ - public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) { + public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + boolean allowRuntimeCacheCreation, String... initialCacheNames) { - Assert.notNull(cacheWriter, "CacheWriter must not be null"); + this(cacheWriter, defaultCacheConfiguration, allowRuntimeCacheCreation); - return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter); + for (String cacheName : initialCacheNames) { + this.initialCacheConfiguration.put(cacheName, defaultCacheConfiguration); + } + } + + /** + * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default + * {@link RedisCacheConfiguration}. + * + * Additionally, an initial {@link RedisCache} will be created and configured using the associated + * {@link RedisCacheConfiguration} for each {@link String named} {@link RedisCache} in the given {@link Map}. + * + * Allows cache creation at runtime. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} + * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with associated + * {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Reds caches} on startup; + * must not be {@literal null}. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter + */ + public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + Map initialCacheConfigurations) { + + this(cacheWriter, defaultCacheConfiguration, DEFAULT_ALLOW_RUNTIME_CACHE_CREATION, initialCacheConfigurations); + } + + /** + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} + * and a default {@link RedisCacheConfiguration}, and whether to allow {@link RedisCache} creation at runtime. + * + * Additionally, an initial {@link RedisCache} will be created and configured using the associated + * {@link RedisCacheConfiguration} for each {@link String named} {@link RedisCache} in the given {@link Map}. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing appropriate Redis commands; must not be {@literal null}. + * @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches} + * by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}. + * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with associated + * {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Redis caches} on startup; + * must not be {@literal null}. + * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} + * are {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheConfiguration + * @see org.springframework.data.redis.cache.RedisCacheWriter + * @since 2.0.4 + */ + public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + boolean allowRuntimeCacheCreation, Map initialCacheConfigurations) { + + this(cacheWriter, defaultCacheConfiguration, allowRuntimeCacheCreation); + + Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null"); + + this.initialCacheConfiguration.putAll(initialCacheConfigurations); + } + + /** + * @deprecated use {@link org.springframework.data.redis.cache.RedisCacheManager#RedisCacheManager(RedisCacheWriter, RedisCacheConfiguration, boolean, Map)} + * instead. + */ + @Deprecated + public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + Map initialCacheConfigurations, boolean allowRuntimeCacheCreation) { + + this(cacheWriter, defaultCacheConfiguration, allowRuntimeCacheCreation, initialCacheConfigurations); + } + + /** + * Determines whether {@link RedisCache Redis caches} are allowed to be created at runtime. + * + * @return a boolean value indicating whether {@link RedisCache Redis caches} are allowed to be created at runtime. + */ + public boolean isAllowRuntimeCacheCreation() { + return this.allowRuntimeCacheCreation; + } + + /** + * Return an {@link Collections#unmodifiableMap(Map) unmodifiable Map} containing {@link String caches name} + * mapped to the {@link RedisCache} {@link RedisCacheConfiguration configuration}. + * + * @return unmodifiable {@link Map} containing {@link String cache name} + * / {@link RedisCacheConfiguration configuration} pairs. + */ + public Map getCacheConfigurations() { + + Map cacheConfigurationMap = new HashMap<>(getCacheNames().size()); + + getCacheNames().forEach(cacheName -> { + RedisCache cache = (RedisCache) lookupCache(cacheName); + cacheConfigurationMap.put(cacheName, cache != null ? cache.getCacheConfiguration() : null); + }); + + return Collections.unmodifiableMap(cacheConfigurationMap); + } + + /** + * Gets the default {@link RedisCacheConfiguration} applied to new {@link RedisCache} instances on creation + * when custom, non-specific {@link RedisCacheConfiguration} was not provided. + * + * @return the default {@link RedisCacheConfiguration}. + */ + protected RedisCacheConfiguration getDefaultCacheConfiguration() { + return this.defaultCacheConfiguration; + } + + /** + * Gets a {@link Map} of {@link String cache names} to {@link RedisCacheConfiguration} objects as the initial set + * of {@link RedisCache Redis caches} to create on startup. + * + * @return a {@link Map} of {@link String cache names} to {@link RedisCacheConfiguration} objects. + */ + protected Map getInitialCacheConfiguration() { + return Collections.unmodifiableMap(this.initialCacheConfiguration); + } + + @Override + protected RedisCache getMissingCache(String name) { + return isAllowRuntimeCacheCreation() ? createRedisCache(name, getDefaultCacheConfiguration()) : null; + } + + /** + * Creates a new {@link RedisCache} with given {@link String name} and {@link RedisCacheConfiguration}. + * + * @param name {@link String name} for the {@link RedisCache}; must not be {@literal null}. + * @param cacheConfiguration {@link RedisCacheConfiguration} used to configure the {@link RedisCache}; + * resolves to the {@link #getDefaultCacheConfiguration()} if {@literal null}. + * @return a new {@link RedisCache} instance; never {@literal null}. + */ + protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfiguration) { + return new RedisCache(name, cacheWriter, resolveCacheConfiguration(cacheConfiguration)); } @Override protected Collection loadCaches() { - List caches = new LinkedList<>(); - - for (Map.Entry entry : initialCacheConfiguration.entrySet()) { - caches.add(createRedisCache(entry.getKey(), entry.getValue())); - } - - return caches; + return getInitialCacheConfiguration().entrySet().stream() + .map(entry -> createRedisCache(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); } - @Override - protected RedisCache getMissingCache(String name) { - return allowInFlightCacheCreation ? createRedisCache(name, defaultCacheConfig) : null; + private RedisCacheConfiguration resolveCacheConfiguration(@Nullable RedisCacheConfiguration cacheConfiguration) { + return cacheConfiguration != null ? cacheConfiguration : getDefaultCacheConfiguration(); } /** - * @return unmodifiable {@link Map} containing cache name / configuration pairs. Never {@literal null}. - */ - public Map getCacheConfigurations() { - - Map configurationMap = new HashMap<>(getCacheNames().size()); - - getCacheNames().forEach(it -> { - - RedisCache cache = RedisCache.class.cast(lookupCache(it)); - configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null); - }); - - return Collections.unmodifiableMap(configurationMap); - } - - /** - * Configuration hook for creating {@link RedisCache} with given name and {@code cacheConfig}. - * - * @param name must not be {@literal null}. - * @param cacheConfig can be {@literal null}. - * @return never {@literal null}. - */ - protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { - return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig); - } - - /** - * Configurator for creating {@link RedisCacheManager}. + * {@literal Builder} for creating a {@link RedisCacheManager}. * * @author Christoph Strobl * @author Mark Paluch * @author Kezhu Wang + * @author John Blum * @since 2.0 */ public static class RedisCacheManagerBuilder { - private @Nullable RedisCacheWriter cacheWriter; - private CacheStatisticsCollector statisticsCollector = CacheStatisticsCollector.none(); - private RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); - private final Map initialCaches = new LinkedHashMap<>(); + /** + * Factory method returning a new {@literal Builder} used to create and configure a {@link RedisCacheManager} + * using the given {@link RedisCacheWriter}. + * + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing + * appropriate Redis commands; must not be {@literal null}. + * @return new {@link RedisCacheManagerBuilder}. + * @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}. + * @see org.springframework.data.redis.cache.RedisCacheWriter + */ + public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWriter) { + return new RedisCacheManagerBuilder(RedisAssertions.requireObject(cacheWriter, + "CacheWriter must not be null")); + } + + /** + * Factory method returning a new {@literal Builder} used to create and configure a {@link RedisCacheManager} + * using the given {@link RedisConnectionFactory}. + * + * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} + * to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @return new {@link RedisCacheManagerBuilder}. + * @throws IllegalArgumentException if the given {@link RedisConnectionFactory} is {@literal null}. + * @see org.springframework.data.redis.connection.RedisConnectionFactory + */ + public static RedisCacheManagerBuilder fromConnectionFactory(RedisConnectionFactory connectionFactory) { + + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter( + RedisAssertions.requireObject(connectionFactory, "ConnectionFactory must not be null")); + + return new RedisCacheManagerBuilder(cacheWriter); + } + + private boolean allowRuntimeCacheCreation = true; private boolean enableTransactions; - boolean allowInFlightCacheCreation = true; + + private CacheStatisticsCollector statisticsCollector = CacheStatisticsCollector.none(); + + private final Map initialCaches = new LinkedHashMap<>(); + + private RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); + + private @Nullable RedisCacheWriter cacheWriter; private RedisCacheManagerBuilder() {} @@ -297,29 +450,52 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager } /** - * Entry point for builder style {@link RedisCacheManager} configuration. + * Configure whether to allow cache creation at runtime. * - * @param connectionFactory must not be {@literal null}. - * @return new {@link RedisCacheManagerBuilder}. + * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; + * {@literal true} by default. + * @return this {@link RedisCacheManagerBuilder}. */ - public static RedisCacheManagerBuilder fromConnectionFactory(RedisConnectionFactory connectionFactory) { - - Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - - return new RedisCacheManagerBuilder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory)); + public RedisCacheManagerBuilder allowCreateOnMissingCache(boolean allowRuntimeCacheCreation) { + this.allowRuntimeCacheCreation = allowRuntimeCacheCreation; + return this; } /** - * Entry point for builder style {@link RedisCacheManager} configuration. + * Disable {@link RedisCache} creation at runtime for unconfigured, undeclared caches. * - * @param cacheWriter must not be {@literal null}. - * @return new {@link RedisCacheManagerBuilder}. + * {@link RedisCacheManager#getMissingCache(String)} returns {@literal null} for any unconfigured {@link Cache} + * instead of a new {@link RedisCache} instance. This allows the + * {@link org.springframework.cache.support.CompositeCacheManager} to participate. + * + * @return this {@link RedisCacheManagerBuilder}. + * @see #allowCreateOnMissingCache(boolean) + * @see #enableCreateOnMissingCache() + * @since 2.0.4 */ - public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWriter) { + public RedisCacheManagerBuilder disableCreateOnMissingCache() { + return allowCreateOnMissingCache(false); + } - Assert.notNull(cacheWriter, "CacheWriter must not be null"); + /** + * Enables {@link RedisCache} creation at runtime for unconfigured, undeclared caches. + * + * @return this {@link RedisCacheManagerBuilder}. + * @see #allowCreateOnMissingCache(boolean) + * @see #disableCreateOnMissingCache() + * @since 2.0.4 + */ + public RedisCacheManagerBuilder enableCreateOnMissingCache() { + return allowCreateOnMissingCache(true); + } - return new RedisCacheManagerBuilder(cacheWriter); + /** + * Returns the default {@link RedisCacheConfiguration}. + * + * @return the default {@link RedisCacheConfiguration}. + */ + public RedisCacheConfiguration cacheDefaults() { + return this.defaultCacheConfiguration; } /** @@ -330,22 +506,12 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager */ public RedisCacheManagerBuilder cacheDefaults(RedisCacheConfiguration defaultCacheConfiguration) { - Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null"); - - this.defaultCacheConfiguration = defaultCacheConfiguration; + this.defaultCacheConfiguration = RedisAssertions.requireObject(defaultCacheConfiguration, + "DefaultCacheConfiguration must not be null"); return this; } - /** - * Returns applied {@link RedisCacheConfiguration}, allow customization base on it. - * - * @return applied {@link RedisCacheConfiguration}. - */ - public RedisCacheConfiguration cacheDefaults() { - return this.defaultCacheConfiguration; - } - /** * Configure a {@link RedisCacheWriter}. * @@ -354,23 +520,17 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager * @since 2.3 */ public RedisCacheManagerBuilder cacheWriter(RedisCacheWriter cacheWriter) { - - Assert.notNull(cacheWriter, "CacheWriter must not be null"); - - this.cacheWriter = cacheWriter; - + this.cacheWriter = RedisAssertions.requireObject(cacheWriter, "CacheWriter must not be null"); return this; } /** - * Enable {@link RedisCache}s to synchronize cache put/evict operations with ongoing Spring-managed transactions. + * Enables cache statistics. * * @return this {@link RedisCacheManagerBuilder}. */ - public RedisCacheManagerBuilder transactionAware() { - - this.enableTransactions = true; - + public RedisCacheManagerBuilder enableStatistics() { + this.statisticsCollector = CacheStatisticsCollector.create(); return this; } @@ -384,9 +544,39 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager */ public RedisCacheManagerBuilder initialCacheNames(Set cacheNames) { - Assert.notNull(cacheNames, "CacheNames must not be null"); + RedisAssertions.requireObject(cacheNames, "CacheNames must not be null") + .forEach(it -> withCacheConfiguration(it, defaultCacheConfiguration)); + + return this; + } + + /** + * Enable {@link RedisCache}s to synchronize cache put/evict operations with ongoing Spring-managed transactions. + * + * @return this {@link RedisCacheManagerBuilder}. + */ + public RedisCacheManagerBuilder transactionAware() { + this.enableTransactions = true; + return this; + } + + /** + * Registers the given {@link String cache name} and {@link RedisCacheConfiguration} used to + * create and configure a {@link RedisCache} on startup. + * + * @param cacheName {@link String name} of the cache to register for creation on startup. + * @param cacheConfiguration {@link RedisCacheConfiguration} used to configure the new cache on startup. + * @return this {@link RedisCacheManagerBuilder}. + * @since 2.2 + */ + public RedisCacheManagerBuilder withCacheConfiguration(String cacheName, + RedisCacheConfiguration cacheConfiguration) { + + Assert.notNull(cacheName, "CacheName must not be null"); + Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null"); + + this.initialCaches.put(cacheName, cacheConfiguration); - cacheNames.forEach(it -> withCacheConfiguration(it, defaultCacheConfiguration)); return this; } @@ -399,57 +589,15 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager public RedisCacheManagerBuilder withInitialCacheConfigurations( Map cacheConfigurations) { - Assert.notNull(cacheConfigurations, "CacheConfigurations must not be null"); - cacheConfigurations.forEach((cacheName, configuration) -> Assert.notNull(configuration, - String.format("RedisCacheConfiguration for cache %s must not be null", cacheName))); + RedisAssertions.requireObject(cacheConfigurations, "CacheConfigurations must not be null") + .forEach((cacheName, cacheConfiguration) -> RedisAssertions.requireObject(cacheConfiguration, + "RedisCacheConfiguration for cache [%s] must not be null", cacheName)); this.initialCaches.putAll(cacheConfigurations); + return this; } - /** - * @param cacheName - * @param cacheConfiguration - * @return this {@link RedisCacheManagerBuilder}. - * @since 2.2 - */ - public RedisCacheManagerBuilder withCacheConfiguration(String cacheName, - RedisCacheConfiguration cacheConfiguration) { - - Assert.notNull(cacheName, "CacheName must not be null"); - Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null"); - - this.initialCaches.put(cacheName, cacheConfiguration); - return this; - } - - /** - * Disable in-flight {@link org.springframework.cache.Cache} creation for unconfigured caches. - *

- * {@link RedisCacheManager#getMissingCache(String)} returns {@literal null} for any unconfigured - * {@link org.springframework.cache.Cache} instead of a new {@link RedisCache} instance. This allows eg. - * {@link org.springframework.cache.support.CompositeCacheManager} to chime in. - * - * @return this {@link RedisCacheManagerBuilder}. - * @since 2.0.4 - */ - public RedisCacheManagerBuilder disableCreateOnMissingCache() { - - this.allowInFlightCacheCreation = false; - return this; - } - - /** - * Get the {@link Set} of cache names for which the builder holds {@link RedisCacheConfiguration configuration}. - * - * @return an unmodifiable {@link Set} holding the name of caches for which a {@link RedisCacheConfiguration - * configuration} has been set. - * @since 2.2 - */ - public Set getConfiguredCaches() { - return Collections.unmodifiableSet(this.initialCaches.keySet()); - } - /** * Get the {@link RedisCacheConfiguration} for a given cache by its name. * @@ -462,13 +610,14 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager } /** - * @return - * @since 2.4 + * Get the {@link Set} of cache names for which the builder holds {@link RedisCacheConfiguration configuration}. + * + * @return an unmodifiable {@link Set} holding the name of caches for which a {@link RedisCacheConfiguration + * configuration} has been set. + * @since 2.2 */ - public RedisCacheManagerBuilder enableStatistics() { - - this.statisticsCollector = CacheStatisticsCollector.create(); - return this; + public Set getConfiguredCaches() { + return Collections.unmodifiableSet(this.initialCaches.keySet()); } /** @@ -478,21 +627,22 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager */ public RedisCacheManager build() { - Assert.state(cacheWriter != null, - "CacheWriter must not be null You can provide one via 'RedisCacheManagerBuilder#cacheWriter(RedisCacheWriter)'"); + Assert.state(cacheWriter != null, "CacheWriter must not be null;" + + " You can provide one via 'RedisCacheManagerBuilder#cacheWriter(RedisCacheWriter)'"); - RedisCacheWriter theCacheWriter = cacheWriter; + RedisCacheWriter resolvedCacheWriter = !CacheStatisticsCollector.none().equals(statisticsCollector) + ? cacheWriter.withStatisticsCollector(statisticsCollector) + : cacheWriter; - if (!statisticsCollector.equals(CacheStatisticsCollector.none())) { - theCacheWriter = cacheWriter.withStatisticsCollector(statisticsCollector); - } + RedisCacheManager cacheManager = newRedisCacheManager(resolvedCacheWriter); - RedisCacheManager cm = new RedisCacheManager(theCacheWriter, defaultCacheConfiguration, initialCaches, - allowInFlightCacheCreation); + cacheManager.setTransactionAware(enableTransactions); - cm.setTransactionAware(enableTransactions); + return cacheManager; + } - return cm; + private RedisCacheManager newRedisCacheManager(RedisCacheWriter cacheWriter) { + return new RedisCacheManager(cacheWriter, cacheDefaults(), allowRuntimeCacheCreation, initialCaches); } } } diff --git a/src/main/java/org/springframework/data/redis/util/RedisAssertions.java b/src/main/java/org/springframework/data/redis/util/RedisAssertions.java new file mode 100644 index 000000000..93a5adf6b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/util/RedisAssertions.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.util; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract utility class for common assertions used in Spring Data Redis. + * + * @author John Blum + * @since 3.1.0 + */ +public abstract class RedisAssertions { + + /** + * Asserts the given {@link Object} is not {@literal null}. + * + * @param {@link Class type} of {@link Object} being asserted. + * @param target {@link Object} to evaluate. + * @param message {@link String} containing the message for the thrown exception. + * @param arguments array of {@link Object} arguments used to format the {@link String message}. + * @return the given {@link Object}. + * @throws IllegalArgumentException if the {@link Object target} is {@literal null}. + * @see #requireObject(Object, Supplier) + */ + public static T requireObject(@Nullable T target, String message, Object... arguments) { + return requireObject(target, () -> String.format(message, arguments)); + } + + /** + * Asserts the given {@link Object} is not {@literal null}. + * + * @param {@link Class type} of {@link Object} being asserted. + * @param target {@link Object} to evaluate. + * @param message {@link Supplier} supplying the message for the thrown exception. + * @return the given {@link Object}. + * @throws IllegalArgumentException if the {@link Object target} is {@literal null}. + */ + public static T requireObject(@Nullable T target, Supplier message) { + Assert.notNull(target, message); + return target; + } +} diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java index 70d0a7280..75befe282 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java @@ -15,8 +15,15 @@ */ package org.springframework.data.redis.cache; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.time.Duration; import java.util.Collections; @@ -25,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.cache.Cache; import org.springframework.cache.transaction.TransactionAwareCacheDecorator; import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder; @@ -90,7 +98,8 @@ class RedisCacheManagerUnitTests { @Test // DATAREDIS-481 void transactionAwareCacheManagerShouldDecoracteCache() { - Cache cache = RedisCacheManager.builder(cacheWriter).transactionAware().build().getCache("decoracted-cache"); + Cache cache = RedisCacheManager.builder(cacheWriter).transactionAware().build() + .getCache("decoracted-cache"); assertThat(cache).isInstanceOfAny(TransactionAwareCacheDecorator.class); assertThat(ReflectionTestUtils.getField(cache, "targetCache")).isInstanceOf(RedisCache.class); @@ -191,15 +200,23 @@ class RedisCacheManagerUnitTests { verify(cacheWriter, never()).withStatisticsCollector(any()); } - @Test // DATAREDIS-481 - void customizeRedisCacheConfigurationBaseOnApplied() { + @Test // PR-2583 + void customizeRedisCacheConfigurationBasedOnDefaultsIsImmutable() { - RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix(); - RedisCacheManagerBuilder cmb = RedisCacheManager.builder().cacheDefaults(configuration); + RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(30)); - cmb.cacheDefaults(cmb.cacheDefaults().entryTtl(Duration.ofSeconds(10))); + RedisCacheManagerBuilder cacheManagerBuilder = RedisCacheManager.builder().cacheDefaults(defaultCacheConfiguration); - assertThat(cmb.cacheDefaults().usePrefix()).isFalse(); - assertThat(cmb.cacheDefaults().getTtl()).isEqualTo(Duration.ofSeconds(10)); + RedisCacheConfiguration customCacheConfiguration = cacheManagerBuilder.cacheDefaults() + .entryTtl(Duration.ofSeconds(10)) + .disableKeyPrefix(); + + assertThat(customCacheConfiguration).isNotSameAs(defaultCacheConfiguration); + assertThat(cacheManagerBuilder.cacheDefaults(customCacheConfiguration)).isSameAs(cacheManagerBuilder); + assertThat(cacheManagerBuilder.cacheDefaults().usePrefix()).isFalse(); + assertThat(cacheManagerBuilder.cacheDefaults().getTtl()).isEqualTo(Duration.ofSeconds(10)); + assertThat(defaultCacheConfiguration.usePrefix()).isTrue(); + assertThat(defaultCacheConfiguration.getTtl()).isEqualTo(Duration.ofMinutes(30)); } } diff --git a/src/test/java/org/springframework/data/redis/util/RedisAssertionsUnitTests.java b/src/test/java/org/springframework/data/redis/util/RedisAssertionsUnitTests.java new file mode 100644 index 000000000..80cd71346 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/util/RedisAssertionsUnitTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit Tests for {@link RedisAssertions}. + * + * @author John Blum + * @see org.junit.jupiter.api.Test + * @see org.springframework.data.redis.util.RedisAssertions + * @since 1.0.0 + */ +@ExtendWith(MockitoExtension.class) +public class RedisAssertionsUnitTests { + + @Mock + private Supplier mockSupplier; + + @Test + public void requireObjectWithMessageAndArgumentsIsSuccessful() { + assertThat(RedisAssertions.requireObject("test", "Test message")).isEqualTo("test"); + } + + @Test + public void requireObjectWithMessageAndArgumentsThrowsIllegalArgumentException() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> RedisAssertions.requireObject(null, "This is a %s", "test")) + .withMessage("This is a test") + .withNoCause(); + } + + @Test + public void requireObjectWithSupplierIsSuccessful() { + + assertThat(RedisAssertions.requireObject("mock", this.mockSupplier)).isEqualTo("mock"); + + verifyNoInteractions(this.mockSupplier); + } + + @Test + public void requireObjectWithSupplierThrowsIllegalArgumentException() { + + doReturn("Mock message").when(this.mockSupplier).get(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> RedisAssertions.requireObject(null, this.mockSupplier)) + .withMessage("Mock message") + .withNoCause(); + + verify(this.mockSupplier, times(1)).get(); + verifyNoMoreInteractions(this.mockSupplier); + } +}