Additionally, refactors and cleans up the Redis cache extension and infrastructure.

Closes #2583
This commit is contained in:
John Blum
2023-05-18 16:52:03 -07:00
parent 9f98f0d5fa
commit 2246a98a93
7 changed files with 852 additions and 485 deletions

View File

@@ -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;
}
}

View File

@@ -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.
* <p>
* {@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> T get(Object key, Callable<T> 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> T getSynchronized(Object key, Callable<T> valueLoader) {
private synchronized @Nullable <T> T getSynchronized(Object key, Callable<T> valueLoader) {
ValueWrapper result = get(key);
if (result != null) {
return (T) result.get();
}
return result != null ? (T) result.get()
: loadCacheValue(key, valueLoader);
}
protected <T> T loadCacheValue(Object key, Callable<T> 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}.
* <p>
* 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;
}
}

View File

@@ -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. <br />
* 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<String> keySerializationPair;
private final SerializationPair<Object> valueSerializationPair;
private final ConversionService conversionService;
@SuppressWarnings("unchecked")
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
ConversionService conversionService) {
this.ttl = ttl;
this.cacheNullValues = cacheNullValues;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
this.valueSerializationPair = (SerializationPair<Object>) 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<String> keySerializationPair;
private final SerializationPair<Object> valueSerializationPair;
@SuppressWarnings("unchecked")
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
ConversionService conversionService) {
this.ttl = ttl;
this.cacheNullValues = cacheNullValues;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
this.valueSerializationPair = (SerializationPair<Object>) 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<?, String> 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<ConverterRegistry> 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:
*
* <ul>
* <li>{@link String} to {@link byte byte[]} using UTF-8 encoding.</li>
* <li>{@link SimpleKey} to {@link String}</li>
* </ul>
*
* @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) {

View File

@@ -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}.
* <p>
* 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.
* <p>
* 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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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.
* <dl>
* <dt>locking</dt>
* <dd>disabled</dd>
@@ -182,113 +118,330 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
* <dd>enabled</dd>
* </dl>
*
* @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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList<>();
for (Map.Entry<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<String, RedisCacheConfiguration> 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<String> 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<String, RedisCacheConfiguration> 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.
* <p>
* {@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<String> 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<String> 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);
}
}
}

View File

@@ -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 <T> {@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> 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 <T> {@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> T requireObject(@Nullable T target, Supplier<String> message) {
Assert.notNull(target, message);
return target;
}
}