Browse Source

Polish WebSession support and tests

pull/35405/head
Sam Brannen 6 months ago
parent
commit
222702f750
  1. 14
      spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java
  2. 24
      spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java
  3. 8
      spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java
  4. 4
      spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java
  5. 42
      spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java

14
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 the web session for the current request.
* return an instance either matching to the session id requested by the * <p>Always guaranteed to return either an instance matching the session id
* client, or with a new session id either because the client did not * requested by the client, or a new session either because the client did not
* specify one or because the underlying session had expired. Use of this * specify a session id or because the underlying session expired.
* method does not automatically create a session. See {@link WebSession} * <p>Use of this method does not automatically create a session. See
* for more details. * {@link WebSession} for more details.
*/ */
Mono<WebSession> getSession(); Mono<WebSession> getSession();

24
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 * Configure the {@link Clock} to use to set the {@code lastAccessTime} on
* session and to calculate if it is expired. * every created session and to calculate if the session has expired.
* <p>This may be useful to align to different timezone or to set the clock * <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))} * back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))}
* in order to simulate session expiration. * in order to simulate session expiration.
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}. * <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() { public Clock getClock() {
return this.clock; return this.clock;
} }
/** /**
* Return the map of sessions with an {@link Collections#unmodifiableMap * Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the
* unmodifiable} wrapper. This could be used for management purposes, to * map of sessions.
* list active sessions, invalidate expired ones, etc. * <p>This could be used for management purposes, to list active sessions,
* to invalidate expired sessions, etc.
* @since 5.0.8 * @since 5.0.8
*/ */
public Map<String, WebSession> getSessions() { 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 * Check for expired sessions and remove them.
* kicked off lazily during calls to {@link #createWebSession() create} or * <p>Typically such checks are kicked off lazily during calls to
* {@link #retrieveSession retrieve}, no less than 60 seconds apart. * {@link #createWebSession()} or {@link #retrieveSession}, no less than 60
* This method can be called to force a check at a specific time. * seconds apart.
* <p>This method can be called to force a check at a specific time.
* @since 5.0.8 * @since 5.0.8
*/ */
public void removeExpiredSessions() { public void removeExpiredSessions() {

8
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 { public interface WebSessionManager {
/** /**
* Return the {@link WebSession} for the given exchange. Always guaranteed * Return the {@link WebSession} for the given exchange.
* to return an instance either matching to the session id requested by the * <p>Always guaranteed to return either an instance matching the session id
* client, or a new session either because the client did not specify one * requested by the client, or a new session either because the client did not
* or because the underlying session expired. * specify a session id or because the underlying session expired.
* @param exchange the current exchange * @param exchange the current exchange
* @return promise for the WebSession * @return promise for the WebSession
*/ */

4
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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. * Return the WebSession for the given id.
* <p><strong>Note:</strong> This method should perform an expiration check, * <p><strong>Note:</strong> This method should perform an expiration check,
* and if it has expired remove the session and return empty. This method * 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 * @param sessionId the session to load
* @return the session, or an empty {@code Mono} * @return the session, or an empty {@code Mono}
*/ */

42
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Clock;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Map;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -35,10 +34,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link InMemoryWebSessionStore}. * Tests for {@link InMemoryWebSessionStore}.
* *
* @author Rob Winch * @author Rob Winch
* @author Sam Brannen
*/ */
class InMemoryWebSessionStoreTests { class InMemoryWebSessionStoreTests {
private InMemoryWebSessionStore store = new InMemoryWebSessionStore(); private final InMemoryWebSessionStore store = new InMemoryWebSessionStore();
@Test @Test
@ -59,7 +59,7 @@ class InMemoryWebSessionStoreTests {
} }
@Test // gh-24027, gh-26958 @Test // gh-24027, gh-26958
public void createSessionDoesNotBlock() { void createSessionDoesNotBlock() {
this.store.createWebSession() this.store.createWebSession()
.doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue()) .doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue())
.block(); .block();
@ -103,7 +103,7 @@ class InMemoryWebSessionStoreTests {
} }
@Test // SPR-17051 @Test // SPR-17051
public void sessionInvalidatedBeforeSave() { void sessionInvalidatedBeforeSave() {
// Request 1 creates session // Request 1 creates session
WebSession session1 = this.store.createWebSession().block(); WebSession session1 = this.store.createWebSession().block();
assertThat(session1).isNotNull(); assertThat(session1).isNotNull();
@ -132,33 +132,31 @@ class InMemoryWebSessionStoreTests {
@Test @Test
void expirationCheckPeriod() { void expirationCheckPeriod() {
DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
Map<?,?> sessions = (Map<?, ?>) accessor.getPropertyValue("sessions");
assertThat(sessions).isNotNull();
// Create 100 sessions // Create 100 sessions
IntStream.range(0, 100).forEach(i -> insertSession()); IntStream.rangeClosed(1, 100).forEach(i -> insertSession());
assertThat(sessions).hasSize(100); 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))); 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(); insertSession();
assertThat(sessions).hasSize(1); assertNumSessions(1);
} }
@Test @Test
void maxSessions() { void maxSessions() {
this.store.setMaxSessions(10);
IntStream.range(0, 10000).forEach(i -> insertSession()); IntStream.rangeClosed(1, 10).forEach(i -> insertSession());
assertThatIllegalStateException().isThrownBy( assertThatIllegalStateException()
this::insertSession) .isThrownBy(this::insertSession)
.withMessage("Max sessions limit reached: 10000"); .withMessage("Max sessions limit reached: 10");
} }
private WebSession insertSession() { private WebSession insertSession() {
WebSession session = this.store.createWebSession().block(); WebSession session = this.store.createWebSession().block();
assertThat(session).isNotNull(); assertThat(session).isNotNull();
@ -167,4 +165,8 @@ class InMemoryWebSessionStoreTests {
return session; return session;
} }
private void assertNumSessions(int numSessions) {
assertThat(store.getSessions()).hasSize(numSessions);
}
} }

Loading…
Cancel
Save