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 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 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);
+ }
+
}