Skip to content

Commit 3fe2337

Browse files
Allow Customizing Redis Session Mapper
Closes gh-2021
1 parent 24390d8 commit 3fe2337

File tree

10 files changed

+687
-40
lines changed

10 files changed

+687
-40
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2014-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.data.redis;
18+
19+
import java.time.Instant;
20+
import java.util.Map;
21+
import java.util.function.BiFunction;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import reactor.core.publisher.Mono;
26+
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.data.redis.core.ReactiveHashOperations;
30+
import org.springframework.data.redis.core.ReactiveRedisOperations;
31+
import org.springframework.session.MapSession;
32+
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
33+
import org.springframework.session.data.redis.ReactiveRedisSessionRepository.RedisSession;
34+
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
35+
import org.springframework.test.context.junit.jupiter.SpringExtension;
36+
import org.springframework.test.util.ReflectionTestUtils;
37+
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
41+
import static org.mockito.ArgumentMatchers.any;
42+
import static org.mockito.BDDMockito.given;
43+
import static org.mockito.BDDMockito.willAnswer;
44+
import static org.mockito.Mockito.spy;
45+
46+
/**
47+
* Key miss error tests for {@link ReactiveRedisSessionRepository}
48+
*
49+
* @author Marcus da Coregio
50+
* @see <a href="https://github.com/spring-projects/spring-session/issues/2021">Related
51+
* GitHub Issue</a>
52+
*/
53+
@ExtendWith(SpringExtension.class)
54+
class ReactiveRedisSessionRepositoryKeyMissITests extends AbstractRedisITests {
55+
56+
private ReactiveRedisSessionRepository sessionRepository;
57+
58+
private ReactiveRedisOperations<String, Object> spyOperations;
59+
60+
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
61+
62+
@Test
63+
void findByIdWhenSessionDeletedWhileSavingDeltaThenThrowIllegalStateException() {
64+
this.context.register(Config.class);
65+
refreshAndPrepareFields();
66+
RedisSession session = createAndSaveSession(Instant.now());
67+
session.setAttribute("new", "value");
68+
69+
ReactiveHashOperations<String, Object, Object> opsForHash = spy(this.spyOperations.opsForHash());
70+
given(this.spyOperations.opsForHash()).willReturn(opsForHash);
71+
willAnswer((invocation) -> this.sessionRepository.deleteById(session.getId())
72+
.then((Mono<Void>) invocation.callRealMethod())).given(opsForHash).putAll(any(), any());
73+
74+
this.sessionRepository.save(session).block();
75+
assertThatIllegalStateException().isThrownBy(() -> this.sessionRepository.findById(session.getId()).block())
76+
.withMessage("creationTime key must not be null");
77+
}
78+
79+
@Test
80+
void findByIdWhenSessionDeletedWhileSavingDeltaAndSafeMapperThenSessionIsNull() {
81+
this.context.register(RedisSessionMapperConfig.class);
82+
refreshAndPrepareFields();
83+
RedisSession session = createAndSaveSession(Instant.now());
84+
session.setAttribute("new", "value");
85+
86+
ReactiveHashOperations<String, Object, Object> opsForHash = spy(this.spyOperations.opsForHash());
87+
given(this.spyOperations.opsForHash()).willReturn(opsForHash);
88+
willAnswer((invocation) -> this.sessionRepository.deleteById(session.getId())
89+
.then((Mono<Void>) invocation.callRealMethod())).given(opsForHash).putAll(any(), any());
90+
91+
this.sessionRepository.save(session).block();
92+
assertThat(this.sessionRepository.findById(session.getId()).block()).isNull();
93+
}
94+
95+
@SuppressWarnings("unchecked")
96+
private void refreshAndPrepareFields() {
97+
this.context.refresh();
98+
this.sessionRepository = this.context.getBean(ReactiveRedisSessionRepository.class);
99+
ReactiveRedisOperations<String, Object> redisOperations = (ReactiveRedisOperations<String, Object>) ReflectionTestUtils
100+
.getField(this.sessionRepository, "sessionRedisOperations");
101+
this.spyOperations = spy(redisOperations);
102+
ReflectionTestUtils.setField(this.sessionRepository, "sessionRedisOperations", this.spyOperations);
103+
}
104+
105+
private RedisSession createAndSaveSession(Instant lastAccessedTime) {
106+
RedisSession session = this.sessionRepository.createSession().block();
107+
session.setLastAccessedTime(lastAccessedTime);
108+
session.setAttribute("attribute1", "value1");
109+
this.sessionRepository.save(session).block();
110+
return this.sessionRepository.findById(session.getId()).block();
111+
}
112+
113+
@Configuration
114+
@EnableRedisWebSession
115+
static class Config extends BaseConfig {
116+
117+
}
118+
119+
@Configuration
120+
@EnableRedisWebSession
121+
static class RedisSessionMapperConfig extends BaseConfig {
122+
123+
@Bean
124+
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> redisSessionRepositoryCustomizer() {
125+
return (redisSessionRepository) -> redisSessionRepository
126+
.setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
127+
}
128+
129+
}
130+
131+
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, Mono<MapSession>> {
132+
133+
private final RedisSessionMapper delegate = new RedisSessionMapper();
134+
135+
private final ReactiveRedisSessionRepository sessionRepository;
136+
137+
SafeRedisSessionMapper(ReactiveRedisSessionRepository sessionRepository) {
138+
this.sessionRepository = sessionRepository;
139+
}
140+
141+
@Override
142+
public Mono<MapSession> apply(String sessionId, Map<String, Object> map) {
143+
return Mono.fromSupplier(() -> this.delegate.apply(sessionId, map)).onErrorResume(
144+
IllegalStateException.class,
145+
(ex) -> this.sessionRepository.deleteById(sessionId).then(Mono.empty()));
146+
}
147+
148+
}
149+
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2014-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.data.redis;
18+
19+
import java.time.Instant;
20+
import java.util.Map;
21+
import java.util.function.BiFunction;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.data.redis.core.BoundHashOperations;
29+
import org.springframework.data.redis.core.RedisOperations;
30+
import org.springframework.session.MapSession;
31+
import org.springframework.session.config.SessionRepositoryCustomizer;
32+
import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession;
33+
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession;
34+
import org.springframework.test.context.junit.jupiter.SpringExtension;
35+
import org.springframework.test.util.ReflectionTestUtils;
36+
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
37+
38+
import static org.assertj.core.api.Assertions.assertThat;
39+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
40+
import static org.mockito.ArgumentMatchers.any;
41+
import static org.mockito.ArgumentMatchers.anyString;
42+
import static org.mockito.BDDMockito.given;
43+
import static org.mockito.BDDMockito.willAnswer;
44+
import static org.mockito.Mockito.spy;
45+
46+
/**
47+
* Key miss error tests for {@link RedisIndexedSessionRepository}
48+
*
49+
* @author Marcus da Coregio
50+
* @see <a href="https://github.com/spring-projects/spring-session/issues/2021">Related
51+
* GitHub Issue</a>
52+
*/
53+
@ExtendWith(SpringExtension.class)
54+
class RedisIndexedSessionRepositoryKeyMissITests extends AbstractRedisITests {
55+
56+
private RedisIndexedSessionRepository sessionRepository;
57+
58+
private RedisOperations<String, Object> spyOperations;
59+
60+
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
61+
62+
@Test
63+
void findByIdWhenSessionDeletedWhileSavingDeltaThenThrowIllegalStateException() {
64+
this.context.register(Config.class);
65+
refreshAndPrepareFields();
66+
RedisSession session = createAndSaveSession(Instant.now());
67+
session.setAttribute("new", "value");
68+
69+
BoundHashOperations<String, Object, Object> opsForHash = spy(this.spyOperations.boundHashOps(anyString()));
70+
given(this.spyOperations.boundHashOps(anyString())).willReturn(opsForHash);
71+
willAnswer((invocation) -> {
72+
this.sessionRepository.deleteById(session.getId());
73+
return invocation.callRealMethod();
74+
}).given(opsForHash).putAll(any());
75+
76+
this.sessionRepository.save(session);
77+
assertThatIllegalStateException().isThrownBy(() -> this.sessionRepository.findById(session.getId()))
78+
.withMessage("creationTime key must not be null");
79+
}
80+
81+
@Test
82+
void findByIdWhenSessionDeletedWhileSavingDeltaAndSafeMapperThenSessionIsNull() {
83+
this.context.register(RedisSessionMapperConfig.class);
84+
refreshAndPrepareFields();
85+
RedisSession session = createAndSaveSession(Instant.now());
86+
session.setAttribute("new", "value");
87+
88+
BoundHashOperations<String, Object, Object> opsForHash = spy(this.spyOperations.boundHashOps(anyString()));
89+
given(this.spyOperations.boundHashOps(anyString())).willReturn(opsForHash);
90+
willAnswer((invocation) -> {
91+
this.sessionRepository.deleteById(session.getId());
92+
return invocation.callRealMethod();
93+
}).given(opsForHash).putAll(any());
94+
95+
this.sessionRepository.save(session);
96+
assertThat(this.sessionRepository.findById(session.getId())).isNull();
97+
}
98+
99+
@SuppressWarnings("unchecked")
100+
private void refreshAndPrepareFields() {
101+
this.context.refresh();
102+
this.sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class);
103+
RedisOperations<String, Object> redisOperations = (RedisOperations<String, Object>) ReflectionTestUtils
104+
.getField(this.sessionRepository, "sessionRedisOperations");
105+
this.spyOperations = spy(redisOperations);
106+
ReflectionTestUtils.setField(this.sessionRepository, "sessionRedisOperations", this.spyOperations);
107+
}
108+
109+
private RedisSession createAndSaveSession(Instant lastAccessedTime) {
110+
RedisSession session = this.sessionRepository.createSession();
111+
session.setLastAccessedTime(lastAccessedTime);
112+
session.setAttribute("attribute1", "value1");
113+
this.sessionRepository.save(session);
114+
return this.sessionRepository.findById(session.getId());
115+
}
116+
117+
@Configuration
118+
@EnableRedisIndexedHttpSession
119+
static class Config extends BaseConfig {
120+
121+
}
122+
123+
@Configuration
124+
@EnableRedisIndexedHttpSession
125+
static class RedisSessionMapperConfig extends BaseConfig {
126+
127+
@Bean
128+
SessionRepositoryCustomizer<RedisIndexedSessionRepository> redisSessionRepositoryCustomizer() {
129+
return (redisSessionRepository) -> redisSessionRepository.setRedisSessionMapper(
130+
new SafeRedisSessionMapper(redisSessionRepository.getSessionRedisOperations()));
131+
}
132+
133+
}
134+
135+
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {
136+
137+
private final RedisSessionMapper delegate = new RedisSessionMapper();
138+
139+
private final RedisOperations<String, Object> redisOperations;
140+
141+
SafeRedisSessionMapper(RedisOperations<String, Object> redisOperations) {
142+
this.redisOperations = redisOperations;
143+
}
144+
145+
@Override
146+
public MapSession apply(String sessionId, Map<String, Object> map) {
147+
try {
148+
return this.delegate.apply(sessionId, map);
149+
}
150+
catch (IllegalStateException ex) {
151+
this.redisOperations.delete("spring:session:sessions:" + sessionId);
152+
return null;
153+
}
154+
}
155+
156+
}
157+
158+
}

0 commit comments

Comments
 (0)