Skip to content

Commit 4e18aa3

Browse files
committed
Add SimpleRedisOperationsSessionRepository
1 parent 849c05e commit 4e18aa3

File tree

3 files changed

+686
-0
lines changed

3 files changed

+686
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2014-2019 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 org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.data.redis.connection.RedisConnectionFactory;
26+
import org.springframework.data.redis.core.RedisTemplate;
27+
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
28+
import org.springframework.test.context.ContextConfiguration;
29+
import org.springframework.test.context.junit4.SpringRunner;
30+
import org.springframework.test.context.web.WebAppConfiguration;
31+
32+
/**
33+
* Integration tests for {@link SimpleRedisOperationsSessionRepository}.
34+
*
35+
* @author Vedran Pavic
36+
*/
37+
@RunWith(SpringRunner.class)
38+
@ContextConfiguration
39+
@WebAppConfiguration
40+
public class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
41+
42+
@Autowired
43+
private SimpleRedisOperationsSessionRepository sessionRepository;
44+
45+
@Test
46+
public void test() {
47+
// TODO
48+
}
49+
50+
@Configuration
51+
@EnableSpringHttpSession
52+
static class Config extends BaseConfig {
53+
54+
@Bean
55+
public SimpleRedisOperationsSessionRepository sessionRepository(
56+
RedisConnectionFactory redisConnectionFactory) {
57+
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
58+
redisTemplate.setConnectionFactory(redisConnectionFactory);
59+
redisTemplate.afterPropertiesSet();
60+
return new SimpleRedisOperationsSessionRepository(redisTemplate);
61+
}
62+
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/*
2+
* Copyright 2014-2019 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.Duration;
20+
import java.time.Instant;
21+
import java.util.Date;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.Set;
25+
26+
import org.springframework.data.redis.core.RedisOperations;
27+
import org.springframework.session.MapSession;
28+
import org.springframework.session.Session;
29+
import org.springframework.session.SessionRepository;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* A {@link SessionRepository} implementation that uses Spring Data's
34+
* {@link RedisOperations} to store sessions is Redis.
35+
* <p>
36+
* This implementation does not support publishing of session events.
37+
*
38+
* @author Vedran Pavic
39+
* @since 2.2.0
40+
*/
41+
public class SimpleRedisOperationsSessionRepository implements
42+
SessionRepository<SimpleRedisOperationsSessionRepository.RedisSession> {
43+
44+
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
45+
46+
private final RedisOperations<String, Object> sessionRedisOperations;
47+
48+
private Duration defaultMaxInactiveInterval = Duration
49+
.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
50+
51+
private String keyNamespace = DEFAULT_KEY_NAMESPACE;
52+
53+
private RedisFlushMode flushMode = RedisFlushMode.ON_SAVE;
54+
55+
public SimpleRedisOperationsSessionRepository(
56+
RedisOperations<String, Object> sessionRedisOperations) {
57+
Assert.notNull(sessionRedisOperations, "sessionRedisOperations mut not be null");
58+
this.sessionRedisOperations = sessionRedisOperations;
59+
}
60+
61+
public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) {
62+
Assert.notNull(defaultMaxInactiveInterval,
63+
"defaultMaxInactiveInterval must not be null");
64+
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
65+
}
66+
67+
public void setKeyNamespace(String keyNamespace) {
68+
Assert.hasText(keyNamespace, "keyNamespace must not be empty");
69+
this.keyNamespace = keyNamespace;
70+
}
71+
72+
public void setFlushMode(RedisFlushMode flushMode) {
73+
Assert.notNull(flushMode, "flushMode must not be null");
74+
this.flushMode = flushMode;
75+
}
76+
77+
@Override
78+
public RedisSession createSession() {
79+
RedisSession session = new RedisSession(this.defaultMaxInactiveInterval);
80+
session.flushIfRequired();
81+
return session;
82+
}
83+
84+
@Override
85+
public void save(RedisSession session) {
86+
if (!session.isNew) {
87+
String key = getSessionKey(session.getId());
88+
Boolean sessionExists = this.sessionRedisOperations.hasKey(key);
89+
if (sessionExists == null || !sessionExists) {
90+
throw new IllegalStateException("Session was invalidated");
91+
}
92+
}
93+
session.save();
94+
}
95+
96+
@Override
97+
public RedisSession findById(String sessionId) {
98+
String key = getSessionKey(sessionId);
99+
Map<String, Object> entries = this.sessionRedisOperations
100+
.<String, Object>opsForHash().entries(key);
101+
if (entries.isEmpty()) {
102+
return null;
103+
}
104+
MapSession session = new RedisSessionMapper(sessionId).apply(entries);
105+
if (session.isExpired()) {
106+
deleteById(sessionId);
107+
return null;
108+
}
109+
return new RedisSession(session);
110+
}
111+
112+
@Override
113+
public void deleteById(String sessionId) {
114+
String key = getSessionKey(sessionId);
115+
this.sessionRedisOperations.delete(key);
116+
}
117+
118+
/**
119+
* Returns the {@link RedisOperations} used for sessions.
120+
* @return the {@link RedisOperations} used for sessions
121+
*/
122+
public RedisOperations<String, Object> getSessionRedisOperations() {
123+
return this.sessionRedisOperations;
124+
}
125+
126+
private String getSessionKey(String sessionId) {
127+
return this.keyNamespace + "sessions:" + sessionId;
128+
}
129+
130+
/**
131+
* An internal {@link Session} implementation used by this {@link SessionRepository}.
132+
*/
133+
final class RedisSession implements Session {
134+
135+
private final MapSession cached;
136+
137+
private Map<String, Object> delta = new HashMap<>();
138+
139+
private boolean isNew;
140+
141+
private String originalSessionId;
142+
143+
RedisSession(Duration maxInactiveInterval) {
144+
this(new MapSession());
145+
this.cached.setMaxInactiveInterval(maxInactiveInterval);
146+
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY,
147+
getCreationTime().toEpochMilli());
148+
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
149+
(int) getMaxInactiveInterval().getSeconds());
150+
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
151+
getLastAccessedTime().toEpochMilli());
152+
this.isNew = true;
153+
}
154+
155+
RedisSession(MapSession cached) {
156+
this.cached = cached;
157+
this.originalSessionId = cached.getId();
158+
}
159+
160+
@Override
161+
public String getId() {
162+
return this.cached.getId();
163+
}
164+
165+
@Override
166+
public String changeSessionId() {
167+
return this.cached.changeSessionId();
168+
}
169+
170+
@Override
171+
public <T> T getAttribute(String attributeName) {
172+
return this.cached.getAttribute(attributeName);
173+
}
174+
175+
@Override
176+
public Set<String> getAttributeNames() {
177+
return this.cached.getAttributeNames();
178+
}
179+
180+
@Override
181+
public void setAttribute(String attributeName, Object attributeValue) {
182+
this.cached.setAttribute(attributeName, attributeValue);
183+
putAttribute(RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName,
184+
attributeValue);
185+
}
186+
187+
@Override
188+
public void removeAttribute(String attributeName) {
189+
setAttribute(attributeName, null);
190+
}
191+
192+
@Override
193+
public Instant getCreationTime() {
194+
return this.cached.getCreationTime();
195+
}
196+
197+
@Override
198+
public void setLastAccessedTime(Instant lastAccessedTime) {
199+
this.cached.setLastAccessedTime(lastAccessedTime);
200+
putAttribute(RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
201+
getLastAccessedTime().toEpochMilli());
202+
}
203+
204+
@Override
205+
public Instant getLastAccessedTime() {
206+
return this.cached.getLastAccessedTime();
207+
}
208+
209+
@Override
210+
public void setMaxInactiveInterval(Duration interval) {
211+
this.cached.setMaxInactiveInterval(interval);
212+
putAttribute(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
213+
(int) getMaxInactiveInterval().getSeconds());
214+
}
215+
216+
@Override
217+
public Duration getMaxInactiveInterval() {
218+
return this.cached.getMaxInactiveInterval();
219+
}
220+
221+
@Override
222+
public boolean isExpired() {
223+
return this.cached.isExpired();
224+
}
225+
226+
private void flushIfRequired() {
227+
if (SimpleRedisOperationsSessionRepository.this.flushMode == RedisFlushMode.IMMEDIATE) {
228+
save();
229+
}
230+
}
231+
232+
private void save() {
233+
saveChangeSessionId();
234+
saveDelta();
235+
if (this.isNew) {
236+
this.isNew = false;
237+
}
238+
}
239+
240+
private void saveChangeSessionId() {
241+
String sessionId = getId();
242+
if (!sessionId.equals(this.originalSessionId)) {
243+
if (!this.isNew) {
244+
String originalSessionIdKey = getSessionKey(this.originalSessionId);
245+
String sessionIdKey = getSessionKey(sessionId);
246+
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations
247+
.rename(originalSessionIdKey, sessionIdKey);
248+
}
249+
this.originalSessionId = sessionId;
250+
}
251+
}
252+
253+
private void saveDelta() {
254+
if (this.delta.isEmpty()) {
255+
return;
256+
}
257+
String key = getSessionKey(getId());
258+
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations
259+
.opsForHash().putAll(key, this.delta);
260+
Long lastAccessedTime = (Long) this.delta
261+
.get(RedisSessionMapper.LAST_ACCESSED_TIME_KEY);
262+
if (lastAccessedTime != null) {
263+
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations
264+
.expireAt(key, Date.from(Instant
265+
.ofEpochMilli(getLastAccessedTime().toEpochMilli())
266+
.plusSeconds(getMaxInactiveInterval().getSeconds())));
267+
}
268+
this.delta = new HashMap<>();
269+
}
270+
271+
private void putAttribute(String name, Object value) {
272+
this.delta.put(name, value);
273+
flushIfRequired();
274+
}
275+
276+
}
277+
278+
}

0 commit comments

Comments
 (0)