From 705e598e01bbeca35084a8739f7377df4c66fb80 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:02:23 +0700 Subject: [PATCH] GH-3857: Allow redisSessionMapper to return null MapSession The BiFunction return type for redisSessionMapper was non-null, but getSession/findById already treat a null mapper result as a missing session. Annotate the return type as @Nullable so Kotlin callers can supply custom mappers that return null (for example when cleaning up corrupt Redis hashes). Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- .../data/redis/RedisIndexedSessionRepository.java | 10 +++++++--- .../session/data/redis/RedisSessionMapper.java | 7 ++++++- .../session/data/redis/RedisSessionRepository.java | 10 +++++++--- .../redis/RedisIndexedSessionRepositoryTests.java | 14 ++++++++++++++ .../data/redis/RedisSessionRepositoryTests.java | 11 +++++++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java index 531e61317..b54beaf24 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java @@ -334,7 +334,7 @@ public class RedisIndexedSessionRepository private SessionIdGenerator sessionIdGenerator = UuidSessionIdGenerator.getInstance(); - private BiFunction, MapSession> redisSessionMapper = new RedisSessionMapper(); + private BiFunction, @Nullable MapSession> redisSessionMapper = new RedisSessionMapper(); /** * Creates a new instance. For an example, refer to the class level javadoc. @@ -757,11 +757,15 @@ public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { /** * Set the {@link BiFunction} used to map {@link MapSession} to a - * {@link ReactiveRedisSessionRepository.RedisSession}. + * {@link ReactiveRedisSessionRepository.RedisSession}. The mapper may return + * {@code null} to indicate that the session could not be loaded (for example when the + * Redis hash is incomplete); callers treat a {@code null} result the same as a + * missing session. * @param redisSessionMapper the mapper to use, cannot be null * @since 3.2 */ - public void setRedisSessionMapper(BiFunction, MapSession> redisSessionMapper) { + public void setRedisSessionMapper( + BiFunction, @Nullable MapSession> redisSessionMapper) { Assert.notNull(redisSessionMapper, "redisSessionMapper cannot be null"); this.redisSessionMapper = redisSessionMapper; } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionMapper.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionMapper.java index 481040671..8c5a6ee7b 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionMapper.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionMapper.java @@ -22,6 +22,8 @@ import java.util.function.BiFunction; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.session.MapSession; import org.springframework.session.Session; import org.springframework.util.Assert; @@ -29,12 +31,15 @@ /** * A {@link Function} that converts a {@link Map} representing Redis hash to a * {@link MapSession}. + *

+ * This implementation never returns {@code null}; custom mappers used via + * {@code setRedisSessionMapper} may return {@code null} to signal an unloadable session. * * @author Vedran Pavic * @author Marcus da Coregio * @since 2.2.0 */ -public final class RedisSessionMapper implements BiFunction, MapSession> { +public final class RedisSessionMapper implements BiFunction, @Nullable MapSession> { /** * The key in the hash representing {@link Session#getCreationTime()}. diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionRepository.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionRepository.java index 1a298ce40..072cb2f2a 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionRepository.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionRepository.java @@ -63,7 +63,7 @@ public class RedisSessionRepository implements SessionRepository, MapSession> redisSessionMapper = new RedisSessionMapper(); + private BiFunction, @Nullable MapSession> redisSessionMapper = new RedisSessionMapper(); /** * Create a new {@link RedisSessionRepository} instance. @@ -183,11 +183,15 @@ public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { /** * Set the {@link BiFunction} used to map {@link MapSession} to a - * {@link ReactiveRedisSessionRepository.RedisSession}. + * {@link ReactiveRedisSessionRepository.RedisSession}. The mapper may return + * {@code null} to indicate that the session could not be loaded (for example when the + * Redis hash is incomplete); callers treat a {@code null} result the same as a + * missing session. * @param redisSessionMapper the mapper to use, cannot be null * @since 3.2 */ - public void setRedisSessionMapper(BiFunction, MapSession> redisSessionMapper) { + public void setRedisSessionMapper( + BiFunction, @Nullable MapSession> redisSessionMapper) { Assert.notNull(redisSessionMapper, "redisSessionMapper cannot be null"); this.redisSessionMapper = redisSessionMapper; } diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java index 1ac304e65..41c54857e 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java @@ -960,6 +960,20 @@ void findByIdWhenChangeSessionIdThenUsesSessionIdGenerator() { assertThat(newSessionId).isEqualTo("test"); } + @Test + void findByIdWhenRedisSessionMapperReturnsNullThenReturnsNull() { + String sessionId = "session-id"; + given(this.redisOperations.boundHashOps(getKey(sessionId))) + .willReturn(this.boundHashOperations); + Map map = map(RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(), + RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 1, RedisSessionMapper.LAST_ACCESSED_TIME_KEY, + Instant.now().toEpochMilli()); + given(this.boundHashOperations.entries()).willReturn(map); + this.redisRepository.setRedisSessionMapper((id, entries) -> null); + + assertThat(this.redisRepository.findById(sessionId)).isNull(); + } + private String getKey(String id) { return "spring:session:sessions:" + id; } diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionRepositoryTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionRepositoryTests.java index e851da723..4db9b02db 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionRepositoryTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionRepositoryTests.java @@ -399,6 +399,17 @@ void findByIdWhenChangeSessionIdThenUsesSessionIdGenerator() { assertThat(session.changeSessionId()).isEqualTo("test"); } + @Test + void findByIdWhenRedisSessionMapperReturnsNullThenReturnsNull() { + given(this.sessionHashOperations.entries(eq(TEST_SESSION_KEY))) + .willReturn(mapOf(RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(), + RedisSessionMapper.LAST_ACCESSED_TIME_KEY, Instant.now().toEpochMilli(), + RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS)); + this.sessionRepository.setRedisSessionMapper((id, entries) -> null); + + assertThat(this.sessionRepository.findById(TEST_SESSION_ID)).isNull(); + } + private static String getSessionKey(String sessionId) { return "spring:session:sessions:" + sessionId; }