Polish WebSession support and tests

This commit is contained in:
Sam Brannen 2025-06-10 11:11:55 +02:00
parent 7bb19fcde8
commit 222702f750
5 changed files with 48 additions and 44 deletions

View File

@ -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.
* <p>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.
* <p>Use of this method does not automatically create a session. See
* {@link WebSession} for more details.
*/
Mono<WebSession> getSession();

View File

@ -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.
* <p>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.
* <p>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.
* <p>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.
* <p>This could be used for management purposes, to list active sessions,
* to invalidate expired sessions, etc.
* @since 5.0.8
*/
public Map<String, WebSession> 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.
* <p>Typically such checks are kicked off lazily during calls to
* {@link #createWebSession()} or {@link #retrieveSession}, no less than 60
* seconds apart.
* <p>This method can be called to force a check at a specific time.
* @since 5.0.8
*/
public void removeExpiredSessions() {

View File

@ -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.
* <p>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
*/

View File

@ -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.
* <p><strong>Note:</strong> 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}
*/

View File

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