From 0a11f3b8580eb08350970847971680c0a8d59031 Mon Sep 17 00:00:00 2001 From: John Blum Date: Thu, 12 Oct 2023 16:05:46 -0700 Subject: [PATCH] Apply additional polishing to Async Caching support. Cleanup ambigious, unreadable, unstructured and complex logic in DefaultRedisCacheWriter. Split RedisCacheTests.retrieveCacheValueUsingJedis() test logic by test case. Edit Javadoc. See #2650 Original pull request: #2717 --- .../redis/cache/DefaultRedisCacheWriter.java | 201 ++++++------ .../data/redis/cache/RedisCache.java | 88 ++--- .../data/redis/cache/RedisCacheManager.java | 176 +++++----- .../data/redis/cache/RedisCacheWriter.java | 40 +-- .../DefaultRedisCachWriterUnitTests.java | 301 ++++++++++++++++++ .../data/redis/cache/RedisCacheTests.java | 34 +- .../data/redis/cache/RedisCacheUnitTests.java | 28 +- 7 files changed, 613 insertions(+), 255 deletions(-) create mode 100644 src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java index 37195ef05d..0137cfc9b0 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -15,20 +15,19 @@ */ package org.springframework.data.redis.cache; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import java.util.function.Function; import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.data.redis.connection.ReactiveRedisConnection; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.ReactiveStringCommands; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; @@ -39,6 +38,10 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; + /** * {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone} * and {@literal cluster} environments, and uses a given {@link RedisConnectionFactory} to obtain the actual @@ -46,11 +49,11 @@ *

* {@link DefaultRedisCacheWriter} can be used in * {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or - * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While - * {@literal non-locking} aims for maximum performance it may result in overlapping, non-atomic, command execution for - * operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents - * command overlap by setting an explicit lock key and checking against presence of this key which leads to additional - * requests and potential command wait times. + * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking} + * aims for maximum performance it may result in overlapping, non-atomic, command execution for operations spanning + * multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap + * by setting an explicit lock key and checking against presence of this key which leads to additional requests + * and potential command wait times. * * @author Christoph Strobl * @author Mark Paluch @@ -60,8 +63,8 @@ */ class DefaultRedisCacheWriter implements RedisCacheWriter { - private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT = ClassUtils - .isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null); + private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT = + ClassUtils.isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null); private final BatchStrategy batchStrategy; @@ -75,31 +78,21 @@ class DefaultRedisCacheWriter implements RedisCacheWriter { private final AsyncCacheWriter asyncCacheWriter; - /** - * @param connectionFactory must not be {@literal null}. - * @param batchStrategy must not be {@literal null}. - */ DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, BatchStrategy batchStrategy) { this(connectionFactory, Duration.ZERO, batchStrategy); } /** - * @param connectionFactory must not be {@literal null}. - * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO} - * to disable locking. - * @param batchStrategy must not be {@literal null}. + * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. + * Use {@link Duration#ZERO} to disable locking. */ DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime, BatchStrategy batchStrategy) { this(connectionFactory, sleepTime, TtlFunction.persistent(), CacheStatisticsCollector.none(), batchStrategy); } /** - * @param connectionFactory must not be {@literal null}. - * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO} - * to disable locking. - * @param lockTtl Lock TTL function must not be {@literal null}. - * @param cacheStatisticsCollector must not be {@literal null}. - * @param batchStrategy must not be {@literal null}. + * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. + * Use {@link Duration#ZERO} to disable locking. */ DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime, TtlFunction lockTtl, CacheStatisticsCollector cacheStatisticsCollector, BatchStrategy batchStrategy) { @@ -160,19 +153,19 @@ public CompletableFuture retrieve(String name, byte[] key, @Nullable Dur Assert.notNull(name, "Name must not be null"); Assert.notNull(key, "Key must not be null"); - return asyncCacheWriter.retrieve(name, key, ttl) // - .thenApply(cachedValue -> { + return asyncCacheWriter.retrieve(name, key, ttl).thenApply(cachedValue -> { - statistics.incGets(name); + statistics.incGets(name); - if (cachedValue != null) { - statistics.incHits(name); - } else { - statistics.incMisses(name); - } + if (cachedValue != null) { + statistics.incHits(name); + } + else { + statistics.incMisses(name); + } - return cachedValue; - }); + return cachedValue; + }); } @Override @@ -185,8 +178,8 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { execute(name, connection -> { if (shouldExpireWithin(ttl)) { - connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), - SetOption.upsert()); + connection.stringCommands() + .set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); } else { connection.stringCommands().set(key, value); } @@ -204,8 +197,7 @@ public CompletableFuture store(String name, byte[] key, byte[] value, @Nul Assert.notNull(key, "Key must not be null"); Assert.notNull(value, "Value must not be null"); - return asyncCacheWriter.store(name, key, value, ttl) // - .thenRun(() -> statistics.incPuts(name)); + return asyncCacheWriter.store(name, key, value, ttl).thenRun(() -> statistics.incPuts(name)); } @Override @@ -226,8 +218,8 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat boolean put; if (shouldExpireWithin(ttl)) { - put = ObjectUtils.nullSafeEquals( - connection.stringCommands().set(key, value, Expiration.from(ttl), SetOption.ifAbsent()), true); + put = ObjectUtils.nullSafeEquals(connection.stringCommands() + .set(key, value, Expiration.from(ttl), SetOption.ifAbsent()), true); } else { put = ObjectUtils.nullSafeEquals(connection.stringCommands().setNX(key, value), true); } @@ -377,12 +369,10 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c Thread.sleep(this.sleepTime.toMillis()); } } catch (InterruptedException cause) { - // Re-interrupt current Thread to allow other participants to react. Thread.currentThread().interrupt(); - - throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), - cause); + String message = String.format("Interrupted while waiting to unlock cache %s", name); + throw new PessimisticLockingFailureException(message, cause); } finally { this.statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs); } @@ -418,8 +408,8 @@ interface AsyncCacheWriter { * @param name the cache name from which to retrieve the cache entry. * @param key the cache entry key. * @param ttl optional TTL to set for Time-to-Idle eviction. - * @return a future that completes either with a value if the value exists or completing with {@code null} if the - * cache does not contain an entry. + * @return a future that completes either with a value if the value exists or completing with {@code null} + * if the cache does not contain an entry. */ CompletableFuture retrieve(String name, byte[] key, @Nullable Duration ttl); @@ -433,6 +423,7 @@ interface AsyncCacheWriter { * @return a future that signals completion. */ CompletableFuture store(String name, byte[] key, byte[] value, @Nullable Duration ttl); + } /** @@ -441,6 +432,7 @@ interface AsyncCacheWriter { * @since 3.2 */ enum UnsupportedAsyncCacheWriter implements AsyncCacheWriter { + INSTANCE; @Override @@ -460,8 +452,8 @@ public CompletableFuture store(String name, byte[] key, byte[] value, @Nul } /** - * Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations using - * {@link ReactiveRedisConnectionFactory}. + * Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations + * using {@link ReactiveRedisConnectionFactory}. * * @since 3.2 */ @@ -478,19 +470,16 @@ public CompletableFuture retrieve(String name, byte[] key, @Nullable Dur return doWithConnection(connection -> { ByteBuffer wrappedKey = ByteBuffer.wrap(key); - Mono cacheLockCheckFlux; - if (isLockingCacheWriter()) - cacheLockCheckFlux = waitForLock(connection, name); - else { - cacheLockCheckFlux = Mono.empty(); - } + Mono cacheLockCheck = isLockingCacheWriter() ? waitForLock(connection, name) : Mono.empty(); + + ReactiveStringCommands stringCommands = connection.stringCommands(); Mono get = shouldExpireWithin(ttl) - ? connection.stringCommands().getEx(wrappedKey, Expiration.from(ttl)) - : connection.stringCommands().get(wrappedKey); + ? stringCommands.getEx(wrappedKey, toExpiration(ttl)) + : stringCommands.get(wrappedKey); - return cacheLockCheckFlux.then(get).map(ByteUtils::getBytes).toFuture(); + return cacheLockCheck.then(get).map(ByteUtils::getBytes).toFuture(); }); } @@ -499,15 +488,9 @@ public CompletableFuture store(String name, byte[] key, byte[] value, @Nul return doWithConnection(connection -> { - Mono mono; - - if (isLockingCacheWriter()) { - - mono = Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection), - unused -> doUnlock(name, connection)); - } else { - mono = doStore(key, value, ttl, connection); - } + Mono mono = isLockingCacheWriter() + ? doLockStoreUnlock(name, key, value, ttl, connection) + : doStore(key, value, ttl, connection); return mono.then().toFuture(); }); @@ -519,24 +502,31 @@ private Mono doStore(byte[] cacheKey, byte[] value, @Nullable Duration ByteBuffer wrappedKey = ByteBuffer.wrap(cacheKey); ByteBuffer wrappedValue = ByteBuffer.wrap(value); - if (shouldExpireWithin(ttl)) { - return connection.stringCommands().set(wrappedKey, wrappedValue, - Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); - } else { - return connection.stringCommands().set(wrappedKey, wrappedValue); - } + ReactiveStringCommands stringCommands = connection.stringCommands(); + + return shouldExpireWithin(ttl) + ? stringCommands.set(wrappedKey, wrappedValue, toExpiration(ttl), SetOption.upsert()) + : stringCommands.set(wrappedKey, wrappedValue); + } + + private Mono doLockStoreUnlock(String name, byte[] key, byte[] value, @Nullable Duration ttl, + ReactiveRedisConnection connection) { + + return Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection), + unused -> doUnlock(name, connection)); } private Mono doLock(String name, Object contextualKey, @Nullable Object contextualValue, ReactiveRedisConnection connection) { - Expiration expiration = Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue)); + ByteBuffer key = ByteBuffer.wrap(createCacheLockKey(name)); + ByteBuffer value = ByteBuffer.wrap(new byte[0]); + + Expiration expiration = toExpiration(contextualKey, contextualValue); - return connection.stringCommands() - .set(ByteBuffer.wrap(createCacheLockKey(name)), ByteBuffer.wrap(new byte[0]), expiration, - SetOption.SET_IF_ABSENT) // - .thenReturn(new Object()); // Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run - // the inner resource function. + return connection.stringCommands().set(key, value, expiration, SetOption.SET_IF_ABSENT) // + // Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run the inner resource function. + .thenReturn(Boolean.TRUE); } private Mono doUnlock(String name, ReactiveRedisConnection connection) { @@ -545,28 +535,59 @@ private Mono doUnlock(String name, ReactiveRedisConnection connection) { private Mono waitForLock(ReactiveRedisConnection connection, String cacheName) { - AtomicLong lockWaitTimeNs = new AtomicLong(); - byte[] cacheLockKey = createCacheLockKey(cacheName); + AtomicLong lockWaitNanoTime = new AtomicLong(); + + Consumer setNanoTimeOnLockWait = subscription -> + lockWaitNanoTime.set(System.nanoTime()); - Flux wait = Flux.interval(Duration.ZERO, sleepTime); - Mono exists = connection.keyCommands().exists(ByteBuffer.wrap(cacheLockKey)).filter(it -> !it); + Consumer recordStatistics = signalType -> + statistics.incLockTime(cacheName, System.nanoTime() - lockWaitNanoTime.get()); - return wait.doOnSubscribe(subscription -> lockWaitTimeNs.set(System.nanoTime())) // - .flatMap(it -> exists) // - .doFinally(signalType -> statistics.incLockTime(cacheName, System.nanoTime() - lockWaitTimeNs.get())) // + Function> doCacheLockExistsCheck = lockWaitTime -> connection.keyCommands() + .exists(toCacheLockKey(cacheName)).filter(cacheLockKeyExists -> !cacheLockKeyExists); + + return waitForLock() // + .doOnSubscribe(setNanoTimeOnLockWait) // + .flatMap(doCacheLockExistsCheck) // + .doFinally(recordStatistics) // .next() // .then(); } + private Flux waitForLock() { + return Flux.interval(Duration.ZERO, sleepTime); + } + + private ByteBuffer toCacheLockKey(String cacheName) { + return ByteBuffer.wrap(createCacheLockKey(cacheName)); + } + + private Expiration toExpiration(Duration ttl) { + return Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS); + } + + private Expiration toExpiration(Object contextualKey, @Nullable Object contextualValue) { + return Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue)); + } + + private ReactiveRedisConnectionFactory getReactiveConnectionFactory() { + return (ReactiveRedisConnectionFactory) DefaultRedisCacheWriter.this.connectionFactory; + } + + private Mono getReactiveConnection() { + return Mono.fromSupplier(getReactiveConnectionFactory()::getReactiveConnection); + } + private CompletableFuture doWithConnection( Function> callback) { - ReactiveRedisConnectionFactory cf = (ReactiveRedisConnectionFactory) connectionFactory; + Function> commandExecution = connection -> + Mono.fromCompletionStage(callback.apply(connection)); + + Mono result = Mono.usingWhen(getReactiveConnection(), commandExecution, + ReactiveRedisConnection::closeLater); - return Mono.usingWhen(Mono.fromSupplier(cf::getReactiveConnection), // - it -> Mono.fromCompletionStage(callback.apply(it)), // - ReactiveRedisConnection::closeLater) // - .toFuture(); + return result.toFuture(); } } } 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 22e1cb9912..9213a80405 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -62,6 +62,9 @@ public class RedisCache extends AbstractValueAdaptingCache { static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); + static final String CACHE_RETRIEVAL_UNSUPPORTED_OPERATION_EXCEPTION_MESSAGE = + "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval"; + private final Lock lock = new ReentrantLock(); private final RedisCacheConfiguration cacheConfiguration; @@ -71,16 +74,16 @@ public class RedisCache extends AbstractValueAdaptingCache { private final String name; /** - * Create a new {@link RedisCache} with the given {@link String name} and {@link RedisCacheConfiguration}, using the - * {@link RedisCacheWriter} to execute Redis commands supporting the cache operations. + * Create a new {@link RedisCache} with the given {@link String name} and {@link RedisCacheConfiguration}, + * using the {@link RedisCacheWriter} to execute Redis commands supporting the cache operations. * * @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 the - * necessary Redis commands; must not be {@literal null}. - * @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache} on creation; must not - * be {@literal null}. + * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations + * by executing the necessary 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}. + * are {@literal null} or the given {@link String} name for this {@link RedisCache} is {@literal null}. */ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfiguration) { @@ -98,28 +101,27 @@ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfig /** * Get the {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization. * - * @return an immutable {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization; - * never {@literal null}. + * @return an immutable {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization. */ public RedisCacheConfiguration getCacheConfiguration() { return this.cacheConfiguration; } /** - * Gets the configured {@link RedisCacheWriter} used to modify Redis for cache operations. + * Gets the configured {@link RedisCacheWriter} used to adapt Redis for cache operations. * - * @return the configured {@link RedisCacheWriter} used to modify Redis for cache operations. + * @return the configured {@link RedisCacheWriter} used to adapt Redis for cache operations. */ protected RedisCacheWriter getCacheWriter() { return this.cacheWriter; } /** - * Gets the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String} when - * accessing entries in the cache. + * Gets the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String} + * when accessing entries in the cache. * - * @return the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String} when - * accessing entries in the cache. + * @return the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String} + * when accessing entries in the cache. * @see RedisCacheConfiguration#getConversionService() * @see #getCacheConfiguration() */ @@ -142,7 +144,7 @@ public RedisCacheWriter getNativeCache() { *

* Statistics are accumulated per cache instance and not from the backing Redis data store. * - * @return statistics object for this {@link RedisCache}. + * @return {@link CacheStatistics} object for this {@link RedisCache}. * @since 2.4 */ public CacheStatistics getStatistics() { @@ -173,8 +175,8 @@ private T getSynchronized(Object key, Callable valueLoader) { } /** - * Loads the {@link Object} using the given {@link Callable valueLoader} and {@link #put(Object, Object) puts} the - * {@link Object loaded value} in the cache. + * Loads the {@link Object} using the given {@link Callable valueLoader} and {@link #put(Object, Object) puts} + * the {@link Object loaded value} in the cache. * * @param {@link Class type} of the loaded {@link Object cache value}. * @param key {@link Object key} mapped to the loaded {@link Object cache value}. @@ -199,9 +201,11 @@ protected T loadCacheValue(Object key, Callable valueLoader) { @Override protected Object lookup(Object key) { + byte[] binaryKey = createAndConvertCacheKey(key); + byte[] value = getCacheConfiguration().isTimeToIdleEnabled() - ? getCacheWriter().get(getName(), createAndConvertCacheKey(key), getTimeToLive(key)) - : getCacheWriter().get(getName(), createAndConvertCacheKey(key)); + ? getCacheWriter().get(getName(), binaryKey, getTimeToLive(key)) + : getCacheWriter().get(getName(), binaryKey); return value != null ? deserializeCacheValue(value) : null; } @@ -219,8 +223,12 @@ public void put(Object key, @Nullable Object value) { Object cacheValue = processAndCheckValue(value); - getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue), - getTimeToLive(key, value)); + byte[] binaryKey = createAndConvertCacheKey(key); + byte[] binaryValue = serializeCacheValue(cacheValue); + + Duration timeToLive = getTimeToLive(key, value); + + getCacheWriter().put(getName(), binaryKey, binaryValue, timeToLive); } @Override @@ -232,8 +240,11 @@ public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { return get(key); } - byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key), - serializeCacheValue(cacheValue), getTimeToLive(key, value)); + Duration timeToLive = getTimeToLive(key, value); + + byte[] binaryKey = createAndConvertCacheKey(key); + byte[] binaryValue = serializeCacheValue(cacheValue); + byte[] result = getCacheWriter().putIfAbsent(getName(), binaryKey, binaryValue, timeToLive); return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null; } @@ -273,8 +284,7 @@ public void evict(Object key) { public CompletableFuture retrieve(Object key) { if (!getCacheWriter().supportsAsyncRetrieve()) { - throw new UnsupportedOperationException( - "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval"); + throw new UnsupportedOperationException(CACHE_RETRIEVAL_UNSUPPORTED_OPERATION_EXCEPTION_MESSAGE); } return retrieveValue(key).thenApply(this::nullSafeDeserializedStoreValue); @@ -285,12 +295,10 @@ public CompletableFuture retrieve(Object key) { public CompletableFuture retrieve(Object key, Supplier> valueLoader) { if (!getCacheWriter().supportsAsyncRetrieve()) { - throw new UnsupportedOperationException( - "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval"); + throw new UnsupportedOperationException(CACHE_RETRIEVAL_UNSUPPORTED_OPERATION_EXCEPTION_MESSAGE); } - return retrieveValue(key) // - .thenCompose(bytes -> { + return retrieveValue(key).thenCompose(bytes -> { if (bytes != null) { return CompletableFuture.completedFuture((T) nullSafeDeserializedStoreValue(bytes)); @@ -300,9 +308,12 @@ public CompletableFuture retrieve(Object key, Supplier value); }); }); @@ -403,8 +414,8 @@ protected String createCacheKey(Object key) { */ protected String convertKey(Object key) { - if (key instanceof String) { - return (String) key; + if (key instanceof String stringKey) { + return stringKey; } TypeDescriptor source = TypeDescriptor.forObject(key); @@ -429,10 +440,9 @@ protected String convertKey(Object key) { return key.toString(); } - 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()); + 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); } 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 84237293e1..4ec25b2f80 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java @@ -25,6 +25,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.support.CompositeCacheManager; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.util.RedisAssertions; @@ -34,8 +35,8 @@ /** * {@link CacheManager} implementation for Redis backed by {@link RedisCache}. *

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

* {@link Cache Caches} requiring a different {@link RedisCacheConfiguration cache configuration} than the * {@link RedisCacheConfiguration#defaultCacheConfig() default cache configuration} can be specified via @@ -46,11 +47,11 @@ * @author Mark Paluch * @author Yanming Zhou * @author John Blum - * @see RedisCache * @see org.springframework.cache.CacheManager * @see org.springframework.data.redis.connection.RedisConnectionFactory * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter + * @see org.springframework.data.redis.cache.RedisCache * @since 2.0 */ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager { @@ -66,17 +67,17 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager private final Map initialCacheConfiguration; /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and a default + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default * {@link RedisCacheConfiguration}. *

* Allows {@link RedisCache 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 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}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter */ @@ -85,17 +86,17 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d } /** - * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. + * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} + * and default {@link RedisCacheConfiguration} along with whether to allow 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 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 specifying whether to allow creation of undeclared caches at runtime; - * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. * @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration} - * are {@literal null}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0.4 @@ -113,19 +114,19 @@ private RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration /** * 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. + * {@link RedisCacheConfiguration} along with an optional, initial set of {@link String cache names} + * used to create {@link RedisCache Redis caches} on startup. *

* Allows {@link RedisCache 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 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. + * 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}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter */ @@ -137,21 +138,21 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d /** * Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter} and default - * {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime. + * {@link RedisCacheConfiguration} along with whether to allow cache creation at runtime. *

- * Additionally, the optional, initial set of {@link String cache names} will be used to create {@link RedisCache - * Redis caches} on startup. + * Additionally, the optional, initial set of {@link String cache names} will 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 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 specifying whether to allow creation of undeclared caches at runtime; - * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * {@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. + * 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}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0.4 @@ -175,15 +176,15 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d *

* Allows {@link RedisCache 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 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}. + * {@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}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter */ @@ -200,17 +201,17 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d * 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 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 specifying whether to allow creation of undeclared caches at runtime; - * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. + * {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}. * @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with the - * associated {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Redis caches} on - * startup; must not be {@literal null}. + * 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}. + * are {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheConfiguration * @see org.springframework.data.redis.cache.RedisCacheWriter * @since 2.0.4 @@ -226,9 +227,7 @@ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration d } /** - * @deprecated since 3.2. Use - * {@link RedisCacheManager#RedisCacheManager(RedisCacheWriter, RedisCacheConfiguration, boolean, Map)} - * instead. + * @deprecated since 3.2. Use {@link RedisCacheManager#RedisCacheManager(RedisCacheWriter, RedisCacheConfiguration, boolean, Map)} instead. */ @Deprecated(since = "3.2") public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, @@ -250,8 +249,8 @@ public static RedisCacheManagerBuilder builder() { * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} * initialized with the given {@link RedisCacheWriter}. * - * @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate - * Redis commands; 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}. * @return new {@link RedisCacheManagerBuilder}. * @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}. * @see org.springframework.data.redis.cache.RedisCacheWriter @@ -267,8 +266,8 @@ public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) { * Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager} * initialized with 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}. + * @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 @@ -298,8 +297,8 @@ public static RedisCacheManagerBuilder builder(RedisConnectionFactory connection *

enabled
* * - * @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager} to acquire - * connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}. + * @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 @@ -308,9 +307,10 @@ public static RedisCacheManager create(RedisConnectionFactory connectionFactory) Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - return new RedisCacheManager( - org.springframework.data.redis.cache.RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), - RedisCacheConfiguration.defaultCacheConfig()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); + + return new RedisCacheManager(cacheWriter, cacheConfiguration); } /** @@ -326,8 +326,8 @@ public boolean isAllowRuntimeCacheCreation() { * 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. + * @return unmodifiable {@link Map} containing {@link String cache name} + * / {@link RedisCacheConfiguration configuration} pairs. */ public Map getCacheConfigurations() { @@ -353,8 +353,8 @@ protected RedisCacheConfiguration getDefaultCacheConfiguration() { } /** - * 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. + * 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. */ @@ -363,8 +363,8 @@ protected Map getInitialCacheConfiguration() { } /** - * Returns a reference to the configured {@link RedisCacheWriter} used to perform {@link RedisCache} operations, such - * as reading from and writing to the cache. + * Returns a reference to the configured {@link RedisCacheWriter} used to perform {@link RedisCache} operations, + * such as reading from and writing to the cache. * * @return a reference to the configured {@link RedisCacheWriter}. * @see org.springframework.data.redis.cache.RedisCacheWriter @@ -382,8 +382,8 @@ protected RedisCache getMissingCache(String name) { * 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}. + * @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) { @@ -416,8 +416,8 @@ public static class RedisCacheManagerBuilder { * 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}. + * @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 @@ -430,16 +430,17 @@ public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWri * 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}. + * @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.requireNonNull(connectionFactory, "ConnectionFactory must not be null")); + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); return new RedisCacheManagerBuilder(cacheWriter); } @@ -464,8 +465,8 @@ private RedisCacheManagerBuilder(RedisCacheWriter cacheWriter) { /** * Configure whether to allow cache creation at runtime. * - * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; {@literal true} by - * default. + * @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime; + * {@literal true} by default. * @return this {@link RedisCacheManagerBuilder}. */ public RedisCacheManagerBuilder allowCreateOnMissingCache(boolean allowRuntimeCacheCreation) { @@ -476,9 +477,9 @@ public RedisCacheManagerBuilder allowCreateOnMissingCache(boolean allowRuntimeCa /** * Disable {@link RedisCache} creation at runtime for non-configured, undeclared caches. *

- * {@link RedisCacheManager#getMissingCache(String)} returns {@literal null} for any non-configured, undeclared - * {@link Cache} instead of a new {@link RedisCache} instance. This allows the - * {@link org.springframework.cache.support.CompositeCacheManager} to participate. + * {@link RedisCacheManager#getMissingCache(String)} returns {@literal null} for any non-configured, + * undeclared {@link Cache} instead of a new {@link RedisCache} instance. + * This allows the {@link CompositeCacheManager} to participate. * * @return this {@link RedisCacheManagerBuilder}. * @see #allowCreateOnMissingCache(boolean) @@ -518,8 +519,9 @@ public RedisCacheConfiguration cacheDefaults() { */ public RedisCacheManagerBuilder cacheDefaults(RedisCacheConfiguration defaultCacheConfiguration) { - this.defaultCacheConfiguration = RedisAssertions.requireNonNull(defaultCacheConfiguration, - "DefaultCacheConfiguration must not be null"); + Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null"); + + this.defaultCacheConfiguration = defaultCacheConfiguration; return this; } @@ -573,8 +575,8 @@ public RedisCacheManagerBuilder transactionAware() { } /** - * Registers the given {@link String cache name} and {@link RedisCacheConfiguration} used to create and configure a - * {@link RedisCache} on startup. + * 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. @@ -624,8 +626,8 @@ public Optional getCacheConfigurationFor(String cacheNa /** * 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. + * @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() { diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java index f6da33eaf7..413521f091 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -24,14 +24,14 @@ import org.springframework.util.Assert; /** - * {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) used for - * caching. + * {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) + * used for caching. *

* The {@link RedisCacheWriter} may be shared by multiple cache implementations and is responsible for reading/writing * binary data from/to Redis. The implementation honors potential cache lock flags that might be set. *

- * The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy} to tune performance - * behavior. + * The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy} + * to tune performance behavior. * * @author Christoph Strobl * @author Mark Paluch @@ -96,8 +96,9 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio * * @param connectionFactory must not be {@literal null}. * @param sleepTime sleep time between lock access attempts, must not be {@literal null}. - * @param lockTtlFunction TTL function to compute the Lock TTL. The function is called with contextual keys and values - * (such as the cache name on cleanup or the actual key/value on put requests). Must not be {@literal null}. + * @param lockTtlFunction TTL function to compute the Lock TTL. The function is called with contextual keys + * and values (such as the cache name on cleanup or the actual key/value on put requests); + * must not be {@literal null}. * @param batchStrategy must not be {@literal null}. * @return new instance of {@link DefaultRedisCacheWriter}. * @since 3.2 @@ -123,8 +124,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio byte[] get(String name, byte[] key); /** - * Get the binary value representation from Redis stored for the given key and set the given {@link Duration TTL - * expiration} for the cache entry. + * Get the binary value representation from Redis stored for the given key and set + * the given {@link Duration TTL expiration} for the cache entry. * * @param name must not be {@literal null}. * @param key must not be {@literal null}. @@ -137,14 +138,14 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) { } /** - * Determines whether the asynchronous {@link #retrieve(String, byte[])} and - * {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation. + * Determines whether the asynchronous {@link #retrieve(String, byte[])} + * and {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation. *

- * The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined by the - * Redis driver in use at runtime. + * The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined + * by the Redis driver in use at runtime. *

- * Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)} and - * {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}. + * Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)} + * and {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}. * * @return {@literal true} if asynchronous {@literal retrieve} operations are supported by the implementation. * @since 3.2 @@ -154,7 +155,8 @@ default boolean supportsAsyncRetrieve() { } /** - * Returns the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}. + * Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} + * maps the given {@link byte[] key}. *

* This operation is non-blocking. * @@ -169,8 +171,8 @@ default CompletableFuture retrieve(String name, byte[] key) { } /** - * Returns the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key} - * setting the {@link Duration TTL expiration} for the cache entry. + * Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps + * the given {@link byte[] key} setting the {@link Duration TTL expiration} for the cache entry. *

* This operation is non-blocking. * @@ -262,8 +264,8 @@ interface TtlFunction { /** * Creates a singleton {@link TtlFunction} using the given {@link Duration}. * - * @param duration the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry does not - * expire). + * @param duration the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry + * does not expire). * @return a singleton {@link TtlFunction} using {@link Duration}. */ static TtlFunction just(Duration duration) { diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java new file mode 100644 index 0000000000..334ed70d33 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCachWriterUnitTests.java @@ -0,0 +1,301 @@ +/* + * 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.cache; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +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.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.data.redis.connection.ReactiveKeyCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.ReactiveStringCommands; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.lang.Nullable; + +import org.assertj.core.api.InstanceOfAssertFactories; + +import reactor.core.publisher.Mono; + +/** + * Unit tests for {@link DefaultRedisCacheWriter} + * + * @author John Blum + */ +@ExtendWith(MockitoExtension.class) +class DefaultRedisCacheWriterUnitTests { + + @Mock + private CacheStatisticsCollector mockCacheStatisticsCollector = mock(CacheStatisticsCollector.class); + + @Mock + private RedisConnection mockConnection; + + @Mock(strictness = Mock.Strictness.LENIENT) + private RedisConnectionFactory mockConnectionFactory; + + @Mock + private ReactiveRedisConnection mockReactiveConnection; + + @Mock(strictness = Mock.Strictness.LENIENT) + private TestReactiveRedisConnectionFactory mockReactiveConnectionFactory; + + @BeforeEach + void setup() { + doReturn(this.mockConnection).when(this.mockConnectionFactory).getConnection(); + doReturn(this.mockConnection).when(this.mockReactiveConnectionFactory).getConnection(); + doReturn(this.mockReactiveConnection).when(this.mockReactiveConnectionFactory).getReactiveConnection(); + } + + private RedisCacheWriter newRedisCacheWriter() { + return spy(new DefaultRedisCacheWriter(this.mockConnectionFactory, mock(BatchStrategy.class)) + .withStatisticsCollector(this.mockCacheStatisticsCollector)); + } + + private RedisCacheWriter newReactiveRedisCacheWriter() { + return spy(new DefaultRedisCacheWriter(this.mockReactiveConnectionFactory, Duration.ZERO, mock(BatchStrategy.class)) + .withStatisticsCollector(this.mockCacheStatisticsCollector)); + } + + @Test // GH-2351 + void getWithNonNullTtl() { + + byte[] key = "TestKey".getBytes(); + byte[] value = "TestValue".getBytes(); + + Duration ttl = Duration.ofSeconds(15); + Expiration expiration = Expiration.from(ttl); + + RedisStringCommands mockStringCommands = mock(RedisStringCommands.class); + + doReturn(mockStringCommands).when(this.mockConnection).stringCommands(); + doReturn(value).when(mockStringCommands).getEx(any(), any()); + + RedisCacheWriter cacheWriter = newRedisCacheWriter(); + + assertThat(cacheWriter.get("TestCache", key, ttl)).isEqualTo(value); + + verify(this.mockConnection, times(1)).stringCommands(); + verify(mockStringCommands, times(1)).getEx(eq(key), eq(expiration)); + verify(this.mockConnection).close(); + verifyNoMoreInteractions(this.mockConnection, mockStringCommands); + } + + @Test // GH-2351 + void getWithNullTtl() { + + byte[] key = "TestKey".getBytes(); + byte[] value = "TestValue".getBytes(); + + RedisStringCommands mockStringCommands = mock(RedisStringCommands.class); + + doReturn(mockStringCommands).when(this.mockConnection).stringCommands(); + doReturn(value).when(mockStringCommands).get(any()); + + RedisCacheWriter cacheWriter = newRedisCacheWriter(); + + assertThat(cacheWriter.get("TestCache", key, null)).isEqualTo(value); + + verify(this.mockConnection, times(1)).stringCommands(); + verify(mockStringCommands, times(1)).get(eq(key)); + verify(this.mockConnection).close(); + verifyNoMoreInteractions(this.mockConnection, mockStringCommands); + } + + @Test // GH-2650 + @SuppressWarnings("all") + void retrieveWithNoCacheName() { + + byte[] key = "TestKey".getBytes(); + + RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> cacheWriter.retrieve(null, key)) + .withMessage("Name must not be null") + .withNoCause(); + + verifyNoInteractions(this.mockReactiveConnectionFactory); + } + + @Test // GH-2650 + @SuppressWarnings("all") + void retrieveWithNoKey() { + + RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> cacheWriter.retrieve("TestCacheName", null)) + .withMessage("Key must not be null") + .withNoCause(); + + verifyNoInteractions(this.mockReactiveConnectionFactory); + } + + @Test // GH-2650 + void retrieveUsingNonReactiveConnectionThrowsUnsupportedOperationException() { + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> newRedisCacheWriter().retrieve("TestCacheName", "TestKey".getBytes())) + .withMessage("async retrieve not supported") + .withNoCause(); + + } + + @Test // GH-2650 + void retrieveWithExpirationUsingNonReactiveConnectionThrowsUnsupportedOperationException() { + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> newRedisCacheWriter().retrieve("TestCacheName", "TestKey".getBytes(), Duration.ofSeconds(30L))) + .withMessage("async retrieve not supported") + .withNoCause(); + + } + + @Test // GH-2650 + @Disabled("Figure out why 'The asyncComplete returned a null Publisher' after the 3.2.0-RC1 release") + void retrieveReturnsReactiveFutureWithNoValue() { + + byte[] key = "TestKey".getBytes(); + + ReactiveKeyCommands keyCommands = withReactiveKeyCommands(it -> + doReturn(Mono.just(false)).when(it).exists(any(ByteBuffer.class))); + + ReactiveStringCommands mockStringCommands = withReactiveStringCommands(it -> + doReturn(Mono.empty()).when(it).get(any(ByteBuffer.class))); + + RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter(); + + assertThat(cacheWriter.retrieve("TestCacheName", key)).isInstanceOf(CompletableFuture.class) + .asInstanceOf(InstanceOfAssertFactories.type(CompletableFuture.class)) + .extracting(this::safeFutureGet) + .isNull(); + + verify(keyCommands, times(1)).exists(any(ByteBuffer.class)); + verify(mockStringCommands, times(1)).get(eq(ByteBuffer.wrap(key))); + verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName")); + verify(this.mockCacheStatisticsCollector, times(1)).incMisses(eq("TestCacheName")); + verifyNoMoreInteractions(keyCommands, mockStringCommands, this.mockCacheStatisticsCollector); + } + + @Test // GH-2650 + @Disabled("Figure out why 'The asyncComplete returned a null Publisher' after the 3.2.0-RC1 release") + void retrieveReturnsReactiveFutureWithValue() throws Exception { + + byte[] key = "TestKey".getBytes(); + + testRetrieveReturnsReactiveFutureWithValue(key, stringCommands -> + verify(stringCommands, times(1)).get(eq(ByteBuffer.wrap(key)))); + } + + @Test // GH-2650 + @Disabled("Figure out why 'The asyncComplete returned a null Publisher' after the 3.2.0-RC1 release") + void retrieveWithExpirationReturnsReactiveFutureWithValue() throws Exception { + + byte[] key = "TestKey".getBytes(); + Duration sixtySeconds = Duration.ofSeconds(60L); + + testRetrieveReturnsReactiveFutureWithValue(key, sixtySeconds, stringCommands -> + verify(stringCommands, times(1)) + .getEx(eq(ByteBuffer.wrap(key)), eq(Expiration.from(sixtySeconds)))); + } + + private void testRetrieveReturnsReactiveFutureWithValue(byte[] key, + Consumer stringCommandsVerification) throws Exception { + + testRetrieveReturnsReactiveFutureWithValue(key, null, stringCommandsVerification); + } + + private void testRetrieveReturnsReactiveFutureWithValue(byte[] key, @Nullable Duration ttl, + Consumer stringCommandsVerification) throws Exception { + + ReactiveKeyCommands mockKeyCommands = withReactiveKeyCommands(it -> + doReturn(Mono.just(false)).when(it).exists(any(ByteBuffer.class))); + + ReactiveStringCommands mockStringCommands = withReactiveStringCommands(it -> + doReturn(Mono.just(ByteBuffer.wrap("test".getBytes()))).when(it).get(ArgumentMatchers.any())); + + RedisCacheWriter cacheWriter = newReactiveRedisCacheWriter(); + + assertThat(cacheWriter.retrieve("TestCacheName", key, ttl)).isInstanceOf(CompletableFuture.class) + .asInstanceOf(InstanceOfAssertFactories.type(CompletableFuture.class)) + .extracting(this::safeFutureGet) + .isEqualTo("test".getBytes()); + + stringCommandsVerification.accept(mockStringCommands); + + verify(mockKeyCommands, times(1)).exists(eq(ByteBuffer.wrap(key))); + verify(this.mockCacheStatisticsCollector, times(1)).incGets(eq("TestCacheName")); + verify(this.mockCacheStatisticsCollector, times(1)).incHits(eq("TestCacheName")); + verifyNoMoreInteractions(mockKeyCommands, mockStringCommands, this.mockCacheStatisticsCollector); + } + + private ReactiveKeyCommands withReactiveKeyCommands(Consumer customizer) { + ReactiveKeyCommands keyCommands = mock(ReactiveKeyCommands.class); + doReturn(keyCommands).when(this.mockReactiveConnection).keyCommands(); + customizer.accept(keyCommands); + return keyCommands; + } + + private ReactiveStringCommands withReactiveStringCommands(Consumer customizer) { + ReactiveStringCommands stringCommands = mock(ReactiveStringCommands.class); + doReturn(stringCommands).when(this.mockReactiveConnection).stringCommands(); + customizer.accept(stringCommands); + return stringCommands; + } + + private @Nullable T safeFutureGet(CompletableFuture future) { + try { + return future.get(); + } + catch (ExecutionException cause) { + throw new DataAccessResourceFailureException("EXECUTION FAILURE", cause); + } + catch (InterruptedException cause) { + throw new DataAccessResourceFailureException("INTERRUPTED", cause); + } + } + + interface TestReactiveRedisConnectionFactory extends ReactiveRedisConnectionFactory, RedisConnectionFactory { } + +} diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index 1cf09b3928..2f3ee56202 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -15,10 +15,10 @@ */ package org.springframework.data.redis.cache; -import static org.assertj.core.api.Assertions.*; -import static org.awaitility.Awaitility.*; - -import io.netty.util.concurrent.DefaultThreadFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.awaitility.Awaitility.await; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -42,6 +42,7 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; + import org.springframework.cache.Cache.ValueWrapper; import org.springframework.cache.interceptor.SimpleKey; import org.springframework.cache.interceptor.SimpleKeyGenerator; @@ -58,6 +59,8 @@ import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; import org.springframework.lang.Nullable; +import io.netty.util.concurrent.DefaultThreadFactory; + /** * Tests for {@link RedisCache} with {@link DefaultRedisCacheWriter} using different {@link RedisSerializer} and * {@link RedisConnectionFactory} pairs. @@ -568,12 +571,18 @@ void cacheGetWithTimeToIdleExpirationAfterEntryExpiresShouldReturnNull() { assertThat(cache.get(this.cacheKey, Person.class)).isNull(); } - @ParameterizedRedisTest + @ParameterizedRedisTest // GH-2650 @EnabledOnRedisDriver(RedisDriver.JEDIS) void retrieveCacheValueUsingJedis() { assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey)).withMessageContaining("RedisCache"); + .isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey, () -> CompletableFuture.completedFuture("TEST"))) + .withMessageContaining("RedisCache"); + } + + @ParameterizedRedisTest // GH-2650 + @EnabledOnRedisDriver(RedisDriver.JEDIS) + void retrieveLoadedValueUsingJedis() { assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> this.cache.retrieve(this.binaryCacheKey, () -> CompletableFuture.completedFuture("TEST"))) @@ -611,9 +620,11 @@ void retrieveReturnsCachedValueWhenLockIsReleased() throws Exception { usingRedisCacheConfiguration()); DefaultRedisCacheWriter cacheWriter = (DefaultRedisCacheWriter) cache.getCacheWriter(); + cacheWriter.lock("cache"); CompletableFuture value = (CompletableFuture) cache.retrieve(this.key); + assertThat(value).isNotDone(); cacheWriter.unlock("cache"); @@ -626,11 +637,12 @@ void retrieveReturnsCachedValueWhenLockIsReleased() throws Exception { @EnabledOnRedisDriver(RedisDriver.LETTUCE) void retrieveReturnsLoadedValue() throws Exception { - RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); AtomicBoolean loaded = new AtomicBoolean(false); Person jon = new Person("Jon", Date.from(Instant.now())); CompletableFuture valueLoader = CompletableFuture.completedFuture(jon); + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); + Supplier> valueLoaderSupplier = () -> { loaded.set(true); return valueLoader; @@ -648,15 +660,15 @@ void retrieveReturnsLoadedValue() throws Exception { @EnabledOnRedisDriver(RedisDriver.LETTUCE) void retrieveStoresLoadedValue() throws Exception { - RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); Person jon = new Person("Jon", Date.from(Instant.now())); Supplier> valueLoaderSupplier = () -> CompletableFuture.completedFuture(jon); + RedisCache cache = new RedisCache("cache", usingLockingRedisCacheWriter(), usingRedisCacheConfiguration()); + cache.retrieve(this.key, valueLoaderSupplier).get(); - doWithConnection( - connection -> assertThat(connection.keyCommands().exists("cache::key-1".getBytes(StandardCharsets.UTF_8))) - .isTrue()); + doWithConnection(connection -> + assertThat(connection.keyCommands().exists("cache::key-1".getBytes(StandardCharsets.UTF_8))).isTrue()); } @ParameterizedRedisTest // GH-2650 diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java index 17663734a3..2188683120 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java @@ -15,13 +15,21 @@ */ package org.springframework.data.redis.cache; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; + import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; /** @@ -33,16 +41,19 @@ class RedisCacheUnitTests { @Test // GH-2650 + @SuppressWarnings("unchecked") void cacheRetrieveValueCallsCacheWriterRetrieveCorrectly() throws Exception { RedisCacheWriter mockCacheWriter = mock(RedisCacheWriter.class); - when(mockCacheWriter.supportsAsyncRetrieve()).thenReturn(true); - when(mockCacheWriter.retrieve(anyString(), any(byte[].class))) - .thenReturn(CompletableFuture.completedFuture("TEST".getBytes())); + doReturn(true).when(mockCacheWriter).supportsAsyncRetrieve(); + doReturn(CompletableFuture.completedFuture("TEST".getBytes())) + .when(mockCacheWriter).retrieve(anyString(), any(byte[].class)); + + RedisCacheConfiguration cacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.byteArray()); - RedisCache cache = new RedisCache("TestCache", mockCacheWriter, - RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.byteArray())); + RedisCache cache = new RedisCache("TestCache", mockCacheWriter, cacheConfiguration); CompletableFuture value = (CompletableFuture) cache.retrieve("TestKey"); @@ -53,5 +64,4 @@ void cacheRetrieveValueCallsCacheWriterRetrieveCorrectly() throws Exception { verify(mockCacheWriter).supportsAsyncRetrieve(); verifyNoMoreInteractions(mockCacheWriter); } - }