From 222702f750b7f98ebabdefd8eea7025849ba8207 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:11:55 +0200 Subject: [PATCH] Polish WebSession support and tests --- .../web/server/ServerWebExchange.java | 14 +++---- .../session/InMemoryWebSessionStore.java | 24 ++++++----- .../web/server/session/WebSessionManager.java | 8 ++-- .../web/server/session/WebSessionStore.java | 4 +- .../session/InMemoryWebSessionStoreTests.java | 42 ++++++++++--------- 5 files changed, 48 insertions(+), 44 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java index da7a3bfb520..b086e62f5be 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -107,12 +107,12 @@ public interface ServerWebExchange { } /** - * Return the web session for the current request. Always guaranteed to - * return an instance either matching to the session id requested by the - * client, or with a new session id either because the client did not - * specify one or because the underlying session had expired. Use of this - * method does not automatically create a session. See {@link WebSession} - * for more details. + * Return the web session for the current request. + *

Always guaranteed to return either an instance matching the session id + * requested by the client, or a new session either because the client did not + * specify a session id or because the underlying session expired. + *

Use of this method does not automatically create a session. See + * {@link WebSession} for more details. */ Mono getSession(); diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index 5644b332fd1..f28fb0e09ae 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -79,9 +79,9 @@ public class InMemoryWebSessionStore implements WebSessionStore { } /** - * Configure the {@link Clock} to use to set lastAccessTime on every created - * session and to calculate if it is expired. - *

This may be useful to align to different timezone or to set the clock + * Configure the {@link Clock} to use to set the {@code lastAccessTime} on + * every created session and to calculate if the session has expired. + *

This may be useful to align to different time zones or to set the clock * back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))} * in order to simulate session expiration. *

By default this is {@code Clock.system(ZoneId.of("GMT"))}. @@ -94,16 +94,17 @@ public class InMemoryWebSessionStore implements WebSessionStore { } /** - * Return the configured clock for session lastAccessTime calculations. + * Return the configured clock for session {@code lastAccessTime} calculations. */ public Clock getClock() { return this.clock; } /** - * Return the map of sessions with an {@link Collections#unmodifiableMap - * unmodifiable} wrapper. This could be used for management purposes, to - * list active sessions, invalidate expired ones, etc. + * Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the + * map of sessions. + *

This could be used for management purposes, to list active sessions, + * to invalidate expired sessions, etc. * @since 5.0.8 */ public Map getSessions() { @@ -157,10 +158,11 @@ public class InMemoryWebSessionStore implements WebSessionStore { } /** - * Check for expired sessions and remove them. Typically such checks are - * kicked off lazily during calls to {@link #createWebSession() create} or - * {@link #retrieveSession retrieve}, no less than 60 seconds apart. - * This method can be called to force a check at a specific time. + * Check for expired sessions and remove them. + *

Typically such checks are kicked off lazily during calls to + * {@link #createWebSession()} or {@link #retrieveSession}, no less than 60 + * seconds apart. + *

This method can be called to force a check at a specific time. * @since 5.0.8 */ public void removeExpiredSessions() { diff --git a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java index 67648eb4e8f..88da7d186de 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java @@ -32,10 +32,10 @@ import org.springframework.web.server.WebSession; public interface WebSessionManager { /** - * Return the {@link WebSession} for the given exchange. Always guaranteed - * to return an instance either matching to the session id requested by the - * client, or a new session either because the client did not specify one - * or because the underlying session expired. + * Return the {@link WebSession} for the given exchange. + *

Always guaranteed to return either an instance matching the session id + * requested by the client, or a new session either because the client did not + * specify a session id or because the underlying session expired. * @param exchange the current exchange * @return promise for the WebSession */ diff --git a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java index 15eeb128425..9a4faa31504 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -43,7 +43,7 @@ public interface WebSessionStore { * Return the WebSession for the given id. *

Note: This method should perform an expiration check, * and if it has expired remove the session and return empty. This method - * should also update the lastAccessTime of retrieved sessions. + * should also update the {@code lastAccessTime} of retrieved sessions. * @param sessionId the session to load * @return the session, or an empty {@code Mono} */ diff --git a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java index 0bf488eda75..726b3a2e530 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -19,7 +19,6 @@ package org.springframework.web.server.session; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Map; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -35,10 +34,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * Tests for {@link InMemoryWebSessionStore}. * * @author Rob Winch + * @author Sam Brannen */ class InMemoryWebSessionStoreTests { - private InMemoryWebSessionStore store = new InMemoryWebSessionStore(); + private final InMemoryWebSessionStore store = new InMemoryWebSessionStore(); @Test @@ -59,7 +59,7 @@ class InMemoryWebSessionStoreTests { } @Test // gh-24027, gh-26958 - public void createSessionDoesNotBlock() { + void createSessionDoesNotBlock() { this.store.createWebSession() .doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue()) .block(); @@ -103,7 +103,7 @@ class InMemoryWebSessionStoreTests { } @Test // SPR-17051 - public void sessionInvalidatedBeforeSave() { + void sessionInvalidatedBeforeSave() { // Request 1 creates session WebSession session1 = this.store.createWebSession().block(); assertThat(session1).isNotNull(); @@ -132,33 +132,31 @@ class InMemoryWebSessionStoreTests { @Test void expirationCheckPeriod() { - - DirectFieldAccessor accessor = new DirectFieldAccessor(this.store); - Map sessions = (Map) accessor.getPropertyValue("sessions"); - assertThat(sessions).isNotNull(); - // Create 100 sessions - IntStream.range(0, 100).forEach(i -> insertSession()); - assertThat(sessions).hasSize(100); + IntStream.rangeClosed(1, 100).forEach(i -> insertSession()); + assertNumSessions(100); - // Force a new clock (31 min later), don't use setter which would clean expired sessions + // Force a new clock (31 min later). Don't use setter which would clean expired sessions. + DirectFieldAccessor accessor = new DirectFieldAccessor(this.store); accessor.setPropertyValue("clock", Clock.offset(this.store.getClock(), Duration.ofMinutes(31))); - assertThat(sessions).hasSize(100); + assertNumSessions(100); - // Create 1 more which forces a time-based check (clock moved forward) + // Create 1 more which forces a time-based check (clock moved forward). insertSession(); - assertThat(sessions).hasSize(1); + assertNumSessions(1); } @Test void maxSessions() { + this.store.setMaxSessions(10); - IntStream.range(0, 10000).forEach(i -> insertSession()); - assertThatIllegalStateException().isThrownBy( - this::insertSession) - .withMessage("Max sessions limit reached: 10000"); + IntStream.rangeClosed(1, 10).forEach(i -> insertSession()); + assertThatIllegalStateException() + .isThrownBy(this::insertSession) + .withMessage("Max sessions limit reached: 10"); } + private WebSession insertSession() { WebSession session = this.store.createWebSession().block(); assertThat(session).isNotNull(); @@ -167,4 +165,8 @@ class InMemoryWebSessionStoreTests { return session; } + private void assertNumSessions(int numSessions) { + assertThat(store.getSessions()).hasSize(numSessions); + } + }