From e42915630f9d37ee5eab6710f2c6912aaff90e32 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Thu, 23 Oct 2025 15:16:30 -0500 Subject: [PATCH 1/2] GH-3223 - Prepare branch --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0e3024e099..541e10043a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 4.0.0-SNAPSHOT + 4.0.0-GH-3223-SNAPSHOT Spring Data Redis Spring Data module for Redis From 444d76415cf76247f427e017ee3538f105e8992a Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Thu, 23 Oct 2025 19:18:39 -0500 Subject: [PATCH 2/2] =?UTF-8?q?Annotate=20ValueOperations.get(=E2=80=A6)?= =?UTF-8?q?=20methods=20with=20`@Nullable`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit annotates the return values of several GET related `ValueOperations` methods with `@Nullable` to reflect the fact that null values are returned when non-existent keys are passed in. Resolves: #3223 Signed-off-by: Chris Bono --- .../redis/core/DefaultValueOperations.java | 25 ++++++------- .../data/redis/core/ValueOperations.java | 20 +++++----- ...efaultValueOperationsIntegrationTests.java | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java index 5ce81a5ee7..bb95e48bad 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -38,6 +38,7 @@ * @author Christoph Strobl * @author Jiahe Cai * @author Ehsan Alemzadeh + * @author Chris Bono */ class DefaultValueOperations extends AbstractOperations implements ValueOperations { @@ -46,37 +47,33 @@ class DefaultValueOperations extends AbstractOperations implements V } @Override - public V get(Object key) { + public @Nullable V get(Object key) { return execute(valueCallbackFor(key, DefaultedRedisConnection::get)); } - @Nullable @Override - public V getAndDelete(K key) { + public @Nullable V getAndDelete(K key) { return execute(valueCallbackFor(key, DefaultedRedisConnection::getDel)); } - @Nullable @Override - public V getAndExpire(K key, long timeout, TimeUnit unit) { + public @Nullable V getAndExpire(K key, long timeout, TimeUnit unit) { return execute( valueCallbackFor(key, (connection, rawKey) -> connection.getEx(rawKey, Expiration.from(timeout, unit)))); } - @Nullable @Override - public V getAndExpire(K key, Duration timeout) { + public @Nullable V getAndExpire(K key, Duration timeout) { return execute(valueCallbackFor(key, (connection, rawKey) -> connection.getEx(rawKey, Expiration.from(timeout)))); } - @Nullable @Override - public V getAndPersist(K key) { + public @Nullable V getAndPersist(K key) { return execute(valueCallbackFor(key, (connection, rawKey) -> connection.getEx(rawKey, Expiration.persistent()))); } @Override - public V getAndSet(K key, V newValue) { + public @Nullable V getAndSet(K key, V newValue) { byte[] rawValue = rawValue(newValue); return execute(valueCallbackFor(key, (connection, rawKey) -> connection.getSet(rawKey, rawValue))); @@ -139,7 +136,7 @@ public String get(K key, long start, long end) { } @Override - public List multiGet(Collection keys) { + public List<@Nullable V> multiGet(Collection keys) { if (keys.isEmpty()) { return Collections.emptyList(); @@ -212,16 +209,16 @@ public void set(K key, V value, long timeout, TimeUnit unit) { } @Override - public V setGet(K key, V value, long timeout, TimeUnit unit) { + public @Nullable V setGet(K key, V value, long timeout, TimeUnit unit) { return doSetGet(key, value, Expiration.from(timeout, unit)); } @Override - public V setGet(K key, V value, Duration duration) { + public @Nullable V setGet(K key, V value, Duration duration) { return doSetGet(key, value, Expiration.from(duration)); } - private V doSetGet(K key, V value, Expiration duration) { + private @Nullable V doSetGet(K key, V value, Expiration duration) { byte[] rawValue = rawValue(value); return execute(new ValueDeserializingRedisCallback(key) { diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperations.java b/src/main/java/org/springframework/data/redis/core/ValueOperations.java index 0c33203ba3..183a64fcb9 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -23,6 +23,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.redis.connection.BitFieldSubCommands; import org.springframework.util.Assert; @@ -34,6 +35,7 @@ * @author Mark Paluch * @author Jiahe Cai * @author Marcin Grzejszczak + * @author Chris Bono */ @NullUnmarked public interface ValueOperations { @@ -60,7 +62,7 @@ public interface ValueOperations { * @see Redis Documentation: SET * @since 3.5 */ - V setGet(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit unit); + @Nullable V setGet(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit unit); /** * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old string stored at key, or @@ -74,7 +76,7 @@ public interface ValueOperations { * @see Redis Documentation: SET * @since 3.5 */ - V setGet(@NonNull K key, @NonNull V value, @NonNull Duration duration); + @Nullable V setGet(@NonNull K key, @NonNull V value, @NonNull Duration duration); /** * Set the {@code value} and expiration {@code timeout} for {@code key}. @@ -226,7 +228,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @return {@literal null} when key does not exist or used in pipeline / transaction. * @see Redis Documentation: GET */ - V get(Object key); + @Nullable V get(Object key); /** * Return the value at {@code key} and delete the key. @@ -236,7 +238,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @see Redis Documentation: GETDEL * @since 2.6 */ - V getAndDelete(@NonNull K key); + @Nullable V getAndDelete(@NonNull K key); /** * Return the value at {@code key} and expire the key by applying {@code timeout}. @@ -248,7 +250,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @see Redis Documentation: GETEX * @since 2.6 */ - V getAndExpire(@NonNull K key, long timeout, @NonNull TimeUnit unit); + @Nullable V getAndExpire(@NonNull K key, long timeout, @NonNull TimeUnit unit); /** * Return the value at {@code key} and expire the key by applying {@code timeout}. @@ -259,7 +261,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @see Redis Documentation: GETEX * @since 2.6 */ - V getAndExpire(@NonNull K key, @NonNull Duration timeout); + @Nullable V getAndExpire(@NonNull K key, @NonNull Duration timeout); /** * Return the value at {@code key} and persist the key. This operation removes any TTL that is associated with @@ -270,7 +272,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @see Redis Documentation: GETEX * @since 2.6 */ - V getAndPersist(@NonNull K key); + @Nullable V getAndPersist(@NonNull K key); /** * Set {@code value} of {@code key} and return its old value. @@ -279,7 +281,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @return {@literal null} when key does not exist or used in pipeline / transaction. * @see Redis Documentation: GETSET */ - V getAndSet(@NonNull K key, @NonNull V value); + @Nullable V getAndSet(@NonNull K key, @NonNull V value); /** * Get multiple {@code keys}. Values are in the order of the requested keys Absent field values are represented using @@ -289,7 +291,7 @@ default Boolean setIfPresent(@NonNull K key, @NonNull V value, @NonNull Duration * @return {@literal null} when used in pipeline / transaction. * @see Redis Documentation: MGET */ - List multiGet(@NonNull Collection<@NonNull K> keys); + List<@Nullable V> multiGet(@NonNull Collection<@NonNull K> keys); /** * Increment an integer value stored as string value under {@code key} by one. diff --git a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java index 51b5b52643..316e29d68c 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java @@ -46,6 +46,7 @@ * @author Jiahe Cai * @author Mark Paluch * @author Hendrik Duerkop + * @author Chris Bono */ @ParameterizedClass @MethodSource("testParams") @@ -153,6 +154,21 @@ void testDecrementByLong() { assertThat(valueOps.get(key)).isEqualTo((Long) value - 5); } + @Test + void testMultiGet() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + K noSuchKey = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key1, value1); + valueOps.set(key2, value2); + + assertThat(valueOps.multiGet(Arrays.asList(key1, noSuchKey, key2))).containsExactly(value1, null, value2); + } + @Test void testMultiSetIfAbsent() { @@ -213,6 +229,9 @@ void testGetSet() { valueOps.set(key, value); assertThat(valueOps.get(key)).isEqualTo(value); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.get(noSuchKey)).isNull(); } @Test // GH-2050 @@ -227,6 +246,9 @@ void testGetAndExpire() { assertThat(valueOps.getAndExpire(key, Duration.ofSeconds(10))).isEqualTo(value1); assertThat(redisTemplate.getExpire(key)).isGreaterThan(1); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.getAndExpire(noSuchKey, Duration.ofSeconds(10))).isNull(); } @Test // GH-2050 @@ -240,6 +262,9 @@ void testGetAndPersist() { assertThat(valueOps.getAndPersist(key)).isEqualTo(value1); assertThat(redisTemplate.getExpire(key)).isEqualTo(-1); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.getAndPersist(noSuchKey)).isNull(); } @Test // GH-2050 @@ -253,6 +278,9 @@ void testGetAndDelete() { assertThat(valueOps.getAndDelete(key)).isEqualTo(value1); assertThat(redisTemplate.hasKey(key)).isFalse(); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.getAndDelete(noSuchKey)).isNull(); } @Test @@ -265,6 +293,9 @@ void testGetAndSet() { valueOps.set(key, value1); assertThat(valueOps.getAndSet(key, value2)).isEqualTo(value1); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.getAndSet(noSuchKey, value2)).isNull(); } @Test @@ -330,6 +361,9 @@ void testSetGetWithExpiration() { assertThat(valueOps.setGet(key, value2, 1, TimeUnit.SECONDS)).isEqualTo(value1); assertThat(valueOps.get(key)).isEqualTo(value2); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.setGet(noSuchKey, value2, 1, TimeUnit.SECONDS)).isNull(); } @Test @@ -343,6 +377,9 @@ void testSetGetWithExpirationDuration() { assertThat(valueOps.setGet(key, value2, Duration.ofMillis(1000))).isEqualTo(value1); assertThat(valueOps.get(key)).isEqualTo(value2); + + K noSuchKey = keyFactory.instance(); + assertThat(valueOps.setGet(noSuchKey, value2, Duration.ofMillis(1000))).isNull(); } @Test