Skip to content

Commit 2c0f01e

Browse files
committed
Merge branch '6.2.x'
2 parents be02d96 + 4d2cc4a commit 2c0f01e

File tree

5 files changed

+90
-46
lines changed

5 files changed

+90
-46
lines changed

spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -106,12 +106,12 @@ default <T> T getAttributeOrDefault(String name, T defaultValue) {
106106
}
107107

108108
/**
109-
* Return the web session for the current request. Always guaranteed to
110-
* return an instance either matching to the session id requested by the
111-
* client, or with a new session id either because the client did not
112-
* specify one or because the underlying session had expired. Use of this
113-
* method does not automatically create a session. See {@link WebSession}
114-
* for more details.
109+
* Return the web session for the current request.
110+
* <p>Always guaranteed to return either an instance matching the session id
111+
* requested by the client, or a new session either because the client did not
112+
* specify a session id or because the underlying session expired.
113+
* <p>Use of this method does not automatically create a session. See
114+
* {@link WebSession} for more details.
115115
*/
116116
Mono<WebSession> getSession();
117117

spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ public int getMaxSessions() {
7979
}
8080

8181
/**
82-
* Configure the {@link Clock} to use to set lastAccessTime on every created
83-
* session and to calculate if it is expired.
84-
* <p>This may be useful to align to different timezone or to set the clock
82+
* Configure the {@link Clock} to use to set the {@code lastAccessTime} on
83+
* every created session and to calculate if the session has expired.
84+
* <p>This may be useful to align to different time zones or to set the clock
8585
* back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))}
8686
* in order to simulate session expiration.
8787
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
@@ -94,16 +94,17 @@ public void setClock(Clock clock) {
9494
}
9595

9696
/**
97-
* Return the configured clock for session lastAccessTime calculations.
97+
* Return the configured clock for session {@code lastAccessTime} calculations.
9898
*/
9999
public Clock getClock() {
100100
return this.clock;
101101
}
102102

103103
/**
104-
* Return the map of sessions with an {@link Collections#unmodifiableMap
105-
* unmodifiable} wrapper. This could be used for management purposes, to
106-
* list active sessions, invalidate expired ones, etc.
104+
* Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the
105+
* map of sessions.
106+
* <p>This could be used for management purposes, to list active sessions,
107+
* to invalidate expired sessions, etc.
107108
* @since 5.0.8
108109
*/
109110
public Map<String, WebSession> getSessions() {
@@ -157,10 +158,11 @@ public Mono<WebSession> updateLastAccessTime(WebSession session) {
157158
}
158159

159160
/**
160-
* Check for expired sessions and remove them. Typically such checks are
161-
* kicked off lazily during calls to {@link #createWebSession() create} or
162-
* {@link #retrieveSession retrieve}, no less than 60 seconds apart.
163-
* This method can be called to force a check at a specific time.
161+
* Check for expired sessions and remove them.
162+
* <p>Typically such checks are kicked off lazily during calls to
163+
* {@link #createWebSession()} or {@link #retrieveSession}, no less than 60
164+
* seconds apart.
165+
* <p>This method can be called to force a check at a specific time.
164166
* @since 5.0.8
165167
*/
166168
public void removeExpiredSessions() {
@@ -281,7 +283,7 @@ public Mono<Void> save() {
281283
private void checkMaxSessionsLimit() {
282284
if (sessions.size() >= maxSessions) {
283285
expiredSessionChecker.removeExpiredSessions(clock.instant());
284-
if (sessions.size() >= maxSessions) {
286+
if (sessions.size() >= maxSessions && !sessions.containsKey(this.id.get())) {
285287
throw new IllegalStateException("Max sessions limit reached: " + sessions.size());
286288
}
287289
}

spring-web/src/main/java/org/springframework/web/server/session/WebSessionManager.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232
public interface WebSessionManager {
3333

3434
/**
35-
* Return the {@link WebSession} for the given exchange. Always guaranteed
36-
* to return an instance either matching to the session id requested by the
37-
* client, or a new session either because the client did not specify one
38-
* or because the underlying session expired.
35+
* Return the {@link WebSession} for the given exchange.
36+
* <p>Always guaranteed to return either an instance matching the session id
37+
* requested by the client, or a new session either because the client did not
38+
* specify a session id or because the underlying session expired.
3939
* @param exchange the current exchange
4040
* @return promise for the WebSession
4141
*/

spring-web/src/main/java/org/springframework/web/server/session/WebSessionStore.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,7 +43,7 @@ public interface WebSessionStore {
4343
* Return the WebSession for the given id.
4444
* <p><strong>Note:</strong> This method should perform an expiration check,
4545
* and if it has expired remove the session and return empty. This method
46-
* should also update the lastAccessTime of retrieved sessions.
46+
* should also update the {@code lastAccessTime} of retrieved sessions.
4747
* @param sessionId the session to load
4848
* @return the session, or an empty {@code Mono}
4949
*/

spring-web/src/test/java/org/springframework/web/server/session/InMemoryWebSessionStoreTests.java

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,11 +19,11 @@
1919
import java.time.Clock;
2020
import java.time.Duration;
2121
import java.time.Instant;
22-
import java.util.Map;
2322
import java.util.stream.IntStream;
2423

2524
import org.junit.jupiter.api.Test;
2625
import reactor.core.scheduler.Schedulers;
26+
import reactor.test.StepVerifier;
2727

2828
import org.springframework.beans.DirectFieldAccessor;
2929
import org.springframework.web.server.WebSession;
@@ -35,10 +35,11 @@
3535
* Tests for {@link InMemoryWebSessionStore}.
3636
*
3737
* @author Rob Winch
38+
* @author Sam Brannen
3839
*/
3940
class InMemoryWebSessionStoreTests {
4041

41-
private InMemoryWebSessionStore store = new InMemoryWebSessionStore();
42+
private final InMemoryWebSessionStore store = new InMemoryWebSessionStore();
4243

4344

4445
@Test
@@ -53,13 +54,14 @@ void startsSessionExplicitly() {
5354
void startsSessionImplicitly() {
5455
WebSession session = this.store.createWebSession().block();
5556
assertThat(session).isNotNull();
56-
session.start();
57+
// We intentionally do not invoke start().
58+
// session.start();
5759
session.getAttributes().put("foo", "bar");
5860
assertThat(session.isStarted()).isTrue();
5961
}
6062

6163
@Test // gh-24027, gh-26958
62-
public void createSessionDoesNotBlock() {
64+
void createSessionDoesNotBlock() {
6365
this.store.createWebSession()
6466
.doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue())
6567
.block();
@@ -103,7 +105,7 @@ void lastAccessTimeIsUpdatedOnRetrieve() {
103105
}
104106

105107
@Test // SPR-17051
106-
public void sessionInvalidatedBeforeSave() {
108+
void sessionInvalidatedBeforeSave() {
107109
// Request 1 creates session
108110
WebSession session1 = this.store.createWebSession().block();
109111
assertThat(session1).isNotNull();
@@ -132,33 +134,69 @@ public void sessionInvalidatedBeforeSave() {
132134

133135
@Test
134136
void expirationCheckPeriod() {
135-
136-
DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
137-
Map<?,?> sessions = (Map<?, ?>) accessor.getPropertyValue("sessions");
138-
assertThat(sessions).isNotNull();
139-
140137
// Create 100 sessions
141-
IntStream.range(0, 100).forEach(i -> insertSession());
142-
assertThat(sessions).hasSize(100);
138+
IntStream.rangeClosed(1, 100).forEach(i -> insertSession());
139+
assertNumSessions(100);
143140

144-
// Force a new clock (31 min later), don't use setter which would clean expired sessions
141+
// Force a new clock (31 min later). Don't use setter which would clean expired sessions.
142+
DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
145143
accessor.setPropertyValue("clock", Clock.offset(this.store.getClock(), Duration.ofMinutes(31)));
146-
assertThat(sessions).hasSize(100);
144+
assertNumSessions(100);
147145

148-
// Create 1 more which forces a time-based check (clock moved forward)
146+
// Create 1 more which forces a time-based check (clock moved forward).
149147
insertSession();
150-
assertThat(sessions).hasSize(1);
148+
assertNumSessions(1);
151149
}
152150

153151
@Test
154152
void maxSessions() {
153+
this.store.setMaxSessions(10);
154+
155+
IntStream.rangeClosed(1, 10).forEach(i -> insertSession());
156+
assertThatIllegalStateException()
157+
.isThrownBy(this::insertSession)
158+
.withMessage("Max sessions limit reached: 10");
159+
}
155160

156-
IntStream.range(0, 10000).forEach(i -> insertSession());
157-
assertThatIllegalStateException().isThrownBy(
158-
this::insertSession)
159-
.withMessage("Max sessions limit reached: 10000");
161+
@Test
162+
void updateSession() {
163+
WebSession session = insertSession();
164+
165+
StepVerifier.create(session.save())
166+
.expectComplete()
167+
.verify();
168+
}
169+
170+
@Test // gh-35013
171+
void updateSessionAfterMaxSessionLimitIsExceeded() {
172+
this.store.setMaxSessions(10);
173+
174+
WebSession session = insertSession();
175+
assertNumSessions(1);
176+
177+
IntStream.rangeClosed(1, 9).forEach(i -> insertSession());
178+
assertNumSessions(10);
179+
180+
// Updating an existing session should succeed.
181+
StepVerifier.create(session.save())
182+
.expectComplete()
183+
.verify();
184+
assertNumSessions(10);
185+
186+
// Saving an additional new session should fail.
187+
assertThatIllegalStateException()
188+
.isThrownBy(this::insertSession)
189+
.withMessage("Max sessions limit reached: 10");
190+
assertNumSessions(10);
191+
192+
// Updating an existing session again should still succeed.
193+
StepVerifier.create(session.save())
194+
.expectComplete()
195+
.verify();
196+
assertNumSessions(10);
160197
}
161198

199+
162200
private WebSession insertSession() {
163201
WebSession session = this.store.createWebSession().block();
164202
assertThat(session).isNotNull();
@@ -167,4 +205,8 @@ private WebSession insertSession() {
167205
return session;
168206
}
169207

208+
private void assertNumSessions(int numSessions) {
209+
assertThat(store.getSessions()).hasSize(numSessions);
210+
}
211+
170212
}

0 commit comments

Comments
 (0)