Skip to content

Commit 8691247

Browse files
rwinchrstoyanchev
authored andcommitted
Refactor WebSessionStore
- Add WebSessionStore.createWebSession. - Remove remove WebSessionStore.changeSessionId - Add WebSessionStore updateLastAccessTime which allows updating the WebSession lastAccessTime without exposing a method on WebSession in an implementation independent way. - Remove WebSessionStore.storeSession. This method is not necessary since the WebSession that is returned allows saving the WebSession. Additionally, it is error prone since the wrong type might be passed into it. Issue: SPR-15875, 15876
1 parent b728047 commit 8691247

File tree

5 files changed

+81
-46
lines changed

5 files changed

+81
-46
lines changed

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
import reactor.core.publisher.Mono;
2525

2626
import org.springframework.util.Assert;
27-
import org.springframework.util.IdGenerator;
28-
import org.springframework.util.JdkIdGenerator;
2927
import org.springframework.web.server.ServerWebExchange;
3028
import org.springframework.web.server.WebSession;
3129

@@ -36,13 +34,11 @@
3634
* {@link WebSessionStore}
3735
*
3836
* @author Rossen Stoyanchev
37+
* @author Rob Winch
3938
* @since 5.0
4039
*/
4140
public class DefaultWebSessionManager implements WebSessionManager {
4241

43-
private static final IdGenerator idGenerator = new JdkIdGenerator();
44-
45-
4642
private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
4743

4844
private WebSessionStore sessionStore = new InMemoryWebSessionStore();
@@ -111,22 +107,20 @@ public Mono<WebSession> getSession(ServerWebExchange exchange) {
111107
return Mono.defer(() ->
112108
retrieveSession(exchange)
113109
.flatMap(session -> removeSessionIfExpired(exchange, session))
114-
.map(session -> {
115-
Instant lastAccessTime = Instant.now(getClock());
116-
return new DefaultWebSession(session, lastAccessTime, s -> saveSession(exchange, s));
117-
})
110+
.flatMap(this.getSessionStore()::updateLastAccessTime)
118111
.switchIfEmpty(createSession(exchange))
112+
.cast(DefaultWebSession.class)
113+
.map(session -> new DefaultWebSession(session, session.getLastAccessTime(), s -> saveSession(exchange, s)))
119114
.doOnNext(session -> exchange.getResponse().beforeCommit(session::save)));
120115
}
121116

122-
private Mono<DefaultWebSession> retrieveSession(ServerWebExchange exchange) {
117+
private Mono<WebSession> retrieveSession(ServerWebExchange exchange) {
123118
return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange))
124119
.concatMap(this.sessionStore::retrieveSession)
125-
.cast(DefaultWebSession.class)
126120
.next();
127121
}
128122

129-
private Mono<DefaultWebSession> removeSessionIfExpired(ServerWebExchange exchange, DefaultWebSession session) {
123+
private Mono<WebSession> removeSessionIfExpired(ServerWebExchange exchange, WebSession session) {
130124
if (session.isExpired()) {
131125
this.sessionIdResolver.expireSession(exchange);
132126
return this.sessionStore.removeSession(session.getId()).then(Mono.empty());
@@ -162,11 +156,7 @@ private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session)
162156
return ids.isEmpty() || !session.getId().equals(ids.get(0));
163157
}
164158

165-
private Mono<DefaultWebSession> createSession(ServerWebExchange exchange) {
166-
return Mono.fromSupplier(() ->
167-
new DefaultWebSession(idGenerator, getClock(),
168-
(oldId, session) -> this.sessionStore.changeSessionId(oldId, session),
169-
session -> saveSession(exchange, session)));
159+
private Mono<WebSession> createSession(ServerWebExchange exchange) {
160+
return this.sessionStore.createWebSession();
170161
}
171-
172162
}

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

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
*/
1616
package org.springframework.web.server.session;
1717

18+
import java.time.Clock;
19+
import java.time.Instant;
20+
import java.time.ZoneId;
1821
import java.util.Map;
1922
import java.util.concurrent.ConcurrentHashMap;
2023

24+
import org.springframework.util.Assert;
25+
import org.springframework.util.IdGenerator;
26+
import org.springframework.util.JdkIdGenerator;
2127
import reactor.core.publisher.Mono;
2228

2329
import org.springframework.web.server.WebSession;
@@ -26,35 +32,73 @@
2632
* Simple Map-based storage for {@link WebSession} instances.
2733
*
2834
* @author Rossen Stoyanchev
35+
* @author Rob Winch
2936
* @since 5.0
3037
*/
3138
public class InMemoryWebSessionStore implements WebSessionStore {
3239

33-
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
40+
private static final IdGenerator idGenerator = new JdkIdGenerator();
3441

42+
private Clock clock = Clock.system(ZoneId.of("GMT"));
43+
44+
private final Map<String, WebSession> sessions = new ConcurrentHashMap<>();
3545

36-
@Override
37-
public Mono<Void> storeSession(WebSession session) {
38-
this.sessions.put(session.getId(), session);
39-
return Mono.empty();
40-
}
4146

4247
@Override
4348
public Mono<WebSession> retrieveSession(String id) {
4449
return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty());
4550
}
4651

4752
@Override
48-
public Mono<Void> changeSessionId(String oldId, WebSession session) {
53+
public Mono<Void> removeSession(String id) {
54+
this.sessions.remove(id);
55+
return Mono.empty();
56+
}
57+
58+
public Mono<WebSession> createWebSession() {
59+
return Mono.fromSupplier(() ->
60+
new DefaultWebSession(idGenerator, getClock(),
61+
(oldId, session) -> this.changeSessionId(oldId, session),
62+
this::storeSession));
63+
}
64+
65+
public Mono<WebSession> updateLastAccessTime(WebSession webSession) {
66+
return Mono.fromSupplier(() -> {
67+
DefaultWebSession session = (DefaultWebSession) webSession;
68+
Instant lastAccessTime = Instant.now(getClock());
69+
return new DefaultWebSession(session, lastAccessTime);
70+
});
71+
}
72+
73+
/**
74+
* Configure the {@link Clock} to use to set lastAccessTime on every created
75+
* session and to calculate if it is expired.
76+
* <p>This may be useful to align to different timezone or to set the clock
77+
* back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))}
78+
* in order to simulate session expiration.
79+
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
80+
* @param clock the clock to use
81+
*/
82+
public void setClock(Clock clock) {
83+
Assert.notNull(clock, "'clock' is required.");
84+
this.clock = clock;
85+
}
86+
87+
/**
88+
* Return the configured clock for session lastAccessTime calculations.
89+
*/
90+
public Clock getClock() {
91+
return this.clock;
92+
}
93+
94+
private Mono<Void> changeSessionId(String oldId, WebSession session) {
4995
this.sessions.remove(oldId);
5096
this.sessions.put(session.getId(), session);
5197
return Mono.empty();
5298
}
5399

54-
@Override
55-
public Mono<Void> removeSession(String id) {
56-
this.sessions.remove(id);
100+
private Mono<Void> storeSession(WebSession session) {
101+
this.sessions.put(session.getId(), session);
57102
return Mono.empty();
58103
}
59-
60104
}

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

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@
1919

2020
import org.springframework.web.server.WebSession;
2121

22+
import java.time.Instant;
23+
2224
/**
2325
* Strategy for {@link WebSession} persistence.
2426
*
2527
* @author Rossen Stoyanchev
28+
* @author Rob Winch
2629
* @since 5.0
2730
*/
2831
public interface WebSessionStore {
2932

3033
/**
31-
* Store the given WebSession.
32-
* @param session the session to store
33-
* @return a completion notification (success or error)
34+
* Creates the WebSession that can be stored by this WebSessionStore.
35+
* @return the session
3436
*/
35-
Mono<Void> storeSession(WebSession session);
37+
Mono<WebSession> createWebSession();
3638

3739
/**
3840
* Return the WebSession for the given id.
@@ -41,23 +43,17 @@ public interface WebSessionStore {
4143
*/
4244
Mono<WebSession> retrieveSession(String sessionId);
4345

44-
/**
45-
* Update WebSession data storage to reflect a change in session id.
46-
* <p>Note that the same can be achieved via a combination of
47-
* {@link #removeSession} + {@link #storeSession}. The purpose of this method
48-
* is to allow a more efficient replacement of the session id mapping
49-
* without replacing and storing the session with all of its data.
50-
* @param oldId the previous session id
51-
* @param session the session reflecting the changed session id
52-
* @return completion notification (success or error)
53-
*/
54-
Mono<Void> changeSessionId(String oldId, WebSession session);
55-
5646
/**
5747
* Remove the WebSession for the specified id.
5848
* @param sessionId the id of the session to remove
5949
* @return a completion notification (success or error)
6050
*/
6151
Mono<Void> removeSession(String sessionId);
6252

53+
/**
54+
* Update the last accessed time to now.
55+
* @param webSession the session to update
56+
* @return the session with the updated last access time
57+
*/
58+
Mono<WebSession> updateLastAccessTime(WebSession webSession);
6359
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public class DefaultWebSessionManagerTests {
7474

7575
@Before
7676
public void setUp() throws Exception {
77+
when(this.store.createWebSession()).thenReturn(Mono.just(createDefaultWebSession()));
78+
when(this.store.updateLastAccessTime(any())).thenAnswer( invocation -> Mono.just(invocation.getArgument(0)));
79+
7780
this.manager = new DefaultWebSessionManager();
7881
this.manager.setSessionIdResolver(this.idResolver);
7982
this.manager.setSessionStore(this.store);
@@ -106,6 +109,7 @@ public void startSessionExplicitly() throws Exception {
106109
session.save().block();
107110

108111
String id = session.getId();
112+
verify(this.store).createWebSession();
109113
verify(this.store).storeSession(any());
110114
verify(this.idResolver).setSessionId(any(), eq(id));
111115
}
@@ -118,6 +122,7 @@ public void startSessionImplicitly() throws Exception {
118122
session.getAttributes().put("foo", "bar");
119123
session.save().block();
120124

125+
verify(this.store).createWebSession();
121126
verify(this.idResolver).setSessionId(any(), any());
122127
verify(this.store).storeSession(any());
123128
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public void expiredSessionIsRecreated() throws Exception {
115115
assertNotNull(session);
116116
Instant lastAccessTime = Clock.offset(this.sessionManager.getClock(), Duration.ofMinutes(-31)).instant();
117117
session = new DefaultWebSession(session, lastAccessTime);
118-
store.storeSession(session);
118+
session.save().block();
119119

120120
// Third request: expired session, new session created
121121
request = RequestEntity.get(createUri()).header("Cookie", "SESSION=" + id).build();

0 commit comments

Comments
 (0)