Add support for session save mode

This commit introduces SaveMode enum that individual SessionRepository implementations use to allow customization of the way they handle save operation in terms of tracking delta of changes.

Resolves: #1466
This commit is contained in:
Vedran Pavic
2019-06-25 21:43:21 +02:00
parent 24b9d24e18
commit 033d6eecae
23 changed files with 812 additions and 177 deletions

View File

@@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.util.Assert;
@@ -59,6 +60,8 @@ public class ReactiveRedisOperationsSessionRepository
*/
private Integer defaultMaxInactiveInterval;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
public ReactiveRedisOperationsSessionRepository(ReactiveRedisOperations<String, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
@@ -90,6 +93,15 @@ public class ReactiveRedisOperationsSessionRepository
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Returns the {@link ReactiveRedisOperations} used for sessions.
* @return the {@link ReactiveRedisOperations} used for sessions
@@ -102,12 +114,11 @@ public class ReactiveRedisOperationsSessionRepository
@Override
public Mono<RedisSession> createSession() {
return Mono.defer(() -> {
RedisSession session = new RedisSession();
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
session.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
return Mono.just(session);
});
}
@@ -132,7 +143,7 @@ public class ReactiveRedisOperationsSessionRepository
.filter((map) -> !map.isEmpty())
.map(new RedisSessionMapper(id))
.filter((session) -> !session.isExpired())
.map(RedisSession::new)
.map((session) -> new RedisSession(session, false))
.switchIfEmpty(Mono.defer(() -> deleteById(id).then(Mono.empty())));
// @formatter:on
}
@@ -168,27 +179,20 @@ public class ReactiveRedisOperationsSessionRepository
private String originalSessionId;
/**
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
*/
RedisSession() {
this(new MapSession());
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.isNew = true;
}
/**
* Creates a new instance from the provided {@link MapSession}.
* @param mapSession the {@link MapSession} that represents the persisted session
* that was retrieved. Cannot be null.
*/
RedisSession(MapSession mapSession) {
Assert.notNull(mapSession, "mapSession cannot be null");
this.cached = mapSession;
this.originalSessionId = mapSession.getId();
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (ReactiveRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
@@ -203,7 +207,12 @@ public class ReactiveRedisOperationsSessionRepository
@Override
public <T> T getAttribute(String attributeName) {
return this.cached.getAttribute(attributeName);
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& ReactiveRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override

View File

@@ -42,6 +42,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
@@ -296,6 +297,8 @@ public class RedisOperationsSessionRepository
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the
@@ -363,6 +366,15 @@ public class RedisOperationsSessionRepository
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
/**
* Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use
@@ -439,7 +451,7 @@ public class RedisOperationsSessionRepository
if (!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded);
RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
@@ -483,9 +495,11 @@ public class RedisOperationsSessionRepository
@Override
public RedisSession createSession() {
Duration maxInactiveInterval = Duration.ofSeconds((this.defaultMaxInactiveInterval != null)
? this.defaultMaxInactiveInterval : MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
RedisSession session = new RedisSession(maxInactiveInterval);
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
@@ -674,32 +688,22 @@ public class RedisOperationsSessionRepository
private String originalSessionId;
/**
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
* @param maxInactiveInterval the amount of time that the {@link Session} should
* be kept alive between client requests.
*/
RedisSession(Duration maxInactiveInterval) {
this(new MapSession());
this.cached.setMaxInactiveInterval(maxInactiveInterval);
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.isNew = true;
}
/**
* Creates a new instance from the provided {@link MapSession}.
* @param cached the {@link MapSession} that represents the persisted session that
* was retrieved. Cannot be null.
*/
RedisSession(MapSession cached) {
Assert.notNull(cached, "MapSession cannot be null");
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (RedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
cached.getAttribute(attributeName)));
}
}
public void setNew(boolean isNew) {
@@ -709,7 +713,8 @@ public class RedisOperationsSessionRepository
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.putAndFlush(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushImmediateIfNecessary();
}
@Override
@@ -744,7 +749,8 @@ public class RedisOperationsSessionRepository
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.putAndFlush(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushImmediateIfNecessary();
}
@Override
@@ -754,7 +760,12 @@ public class RedisOperationsSessionRepository
@Override
public <T> T getAttribute(String attributeName) {
return this.cached.getAttribute(attributeName);
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& RedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
@@ -765,13 +776,15 @@ public class RedisOperationsSessionRepository
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.putAndFlush(getSessionAttrNameKey(attributeName), attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.putAndFlush(getSessionAttrNameKey(attributeName), null);
this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
@@ -780,11 +793,6 @@ public class RedisOperationsSessionRepository
}
}
private void putAndFlush(String a, Object v) {
this.delta.put(a, v);
this.flushImmediateIfNecessary();
}
private void save() {
saveChangeSessionId();
saveDelta();

View File

@@ -26,6 +26,7 @@ import java.util.Set;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.util.Assert;
@@ -52,6 +53,8 @@ public class SimpleRedisOperationsSessionRepository
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link SimpleRedisOperationsSessionRepository} instance.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing
@@ -89,9 +92,20 @@ public class SimpleRedisOperationsSessionRepository
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public RedisSession createSession() {
RedisSession session = new RedisSession(this.defaultMaxInactiveInterval);
MapSession cached = new MapSession();
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
RedisSession session = new RedisSession(cached, true);
session.flushIfRequired();
return session;
}
@@ -120,7 +134,7 @@ public class SimpleRedisOperationsSessionRepository
deleteById(sessionId);
return null;
}
return new RedisSession(session);
return new RedisSession(session, false);
}
@Override
@@ -141,6 +155,10 @@ public class SimpleRedisOperationsSessionRepository
return this.keyNamespace + "sessions:" + sessionId;
}
private static String getAttributeKey(String attributeName) {
return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName;
}
/**
* An internal {@link Session} implementation used by this {@link SessionRepository}.
*/
@@ -154,18 +172,20 @@ public class SimpleRedisOperationsSessionRepository
private String originalSessionId;
RedisSession(Duration maxInactiveInterval) {
this(new MapSession());
this.cached.setMaxInactiveInterval(maxInactiveInterval);
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.isNew = true;
}
RedisSession(MapSession cached) {
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (SimpleRedisOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getAttributeKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
@@ -180,7 +200,12 @@ public class SimpleRedisOperationsSessionRepository
@Override
public <T> T getAttribute(String attributeName) {
return this.cached.getAttribute(attributeName);
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& SimpleRedisOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getAttributeKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
@@ -191,7 +216,8 @@ public class SimpleRedisOperationsSessionRepository
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
putAttribute(RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName, attributeValue);
this.delta.put(getAttributeKey(attributeName), attributeValue);
flushIfRequired();
}
@Override
@@ -207,7 +233,8 @@ public class SimpleRedisOperationsSessionRepository
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
putAttribute(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushIfRequired();
}
@Override
@@ -218,7 +245,8 @@ public class SimpleRedisOperationsSessionRepository
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
putAttribute(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushIfRequired();
}
@Override
@@ -274,11 +302,6 @@ public class SimpleRedisOperationsSessionRepository
this.delta.clear();
}
private void putAttribute(String name, Object value) {
this.delta.put(name, value);
flushIfRequired();
}
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
@@ -119,4 +120,12 @@ public @interface EnableRedisHttpSession {
*/
String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
/**
* Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
* only saves changes made to session.
* @return the save mode
* @since 2.2.0
*/
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}

View File

@@ -50,6 +50,7 @@ import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
@@ -86,6 +87,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
@@ -117,6 +120,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
return sessionRepository;
@@ -165,6 +169,10 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.flushMode = flushMode;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
@@ -240,6 +248,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
flushMode = redisFlushMode.getFlushMode();
}
this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;

View File

@@ -27,6 +27,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
@@ -98,4 +99,12 @@ public @interface EnableRedisWebSession {
@Deprecated
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
/**
* Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
* only saves changes made to session.
* @return the save mode
* @since 2.2.0
*/
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}

View File

@@ -35,6 +35,7 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisFlushMode;
@@ -61,6 +62,8 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
private String redisNamespace = ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private ReactiveRedisConnectionFactory redisConnectionFactory;
private RedisSerializer<Object> defaultRedisSerializer;
@@ -78,6 +81,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setSaveMode(this.saveMode);
return sessionRepository;
}
@@ -94,6 +98,10 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<ReactiveRedisConnectionFactory> springSessionRedisConnectionFactory,
@@ -132,6 +140,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
if (StringUtils.hasText(redisNamespaceValue)) {
this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue);
}
this.saveMode = attributes.getEnum("saveMode");
}
private ReactiveRedisTemplate<String, Object> createReactiveRedisTemplate() {

View File

@@ -32,6 +32,7 @@ import reactor.test.StepVerifier;
import org.springframework.data.redis.core.ReactiveHashOperations;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils;
@@ -130,7 +131,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
RedisSession newSession = this.repository.new RedisSession();
RedisSession newSession = this.repository.new RedisSession(new MapSession(), true);
StepVerifier.create(this.repository.save(newSession)).verifyComplete();
verify(this.redisOperations).opsForHash();
@@ -154,7 +155,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
RedisSession session = this.repository.new RedisSession(new MapSession(this.cached));
RedisSession session = this.repository.new RedisSession(this.cached, false);
StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -170,7 +171,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
RedisSession session = this.repository.new RedisSession(this.cached);
RedisSession session = this.repository.new RedisSession(this.cached, false);
session.setLastAccessedTime(Instant.ofEpochMilli(12345678L));
StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -193,7 +194,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
String attrName = "attrName";
RedisSession session = this.repository.new RedisSession(this.cached);
RedisSession session = this.repository.new RedisSession(this.cached, false);
session.setAttribute(attrName, "attrValue");
StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -216,7 +217,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
String attrName = "attrName";
RedisSession session = this.repository.new RedisSession(new MapSession());
RedisSession session = this.repository.new RedisSession(new MapSession(), false);
session.removeAttribute(attrName);
StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -234,7 +235,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@Test
void redisSessionGetAttributes() {
String attrName = "attrName";
RedisSession session = this.repository.new RedisSession(this.cached);
RedisSession session = this.repository.new RedisSession(this.cached, false);
assertThat(session.getAttributeNames()).isEmpty();
session.setAttribute(attrName, "attrValue");
@@ -325,7 +326,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@Test // gh-1120
void getAttributeNamesAndRemove() {
RedisSession session = this.repository.new RedisSession(this.cached);
RedisSession session = this.repository.new RedisSession(this.cached, false);
session.setAttribute("attribute1", "value1");
session.setAttribute("attribute2", "value2");
@@ -336,6 +337,78 @@ class ReactiveRedisOperationsSessionRepositoryTests {
assertThat(session.getAttributeNames()).isEmpty();
}
@Test
void saveWithSaveModeOnSetAttribute() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
this.repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
RedisSession session = this.repository.new RedisSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
StepVerifier.create(this.repository.save(session)).verifyComplete();
verify(this.redisOperations).hasKey(anyString());
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(1);
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
}
@Test
void saveWithSaveModeOnGetAttribute() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
this.repository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
RedisSession session = this.repository.new RedisSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
StepVerifier.create(this.repository.save(session)).verifyComplete();
verify(this.redisOperations).hasKey(anyString());
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(2);
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
}
@Test
void saveWithSaveModeAlways() {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.opsForHash()).willReturn(this.hashOperations);
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
this.repository.setSaveMode(SaveMode.ALWAYS);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
RedisSession session = this.repository.new RedisSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
StepVerifier.create(this.repository.save(session)).verifyComplete();
verify(this.redisOperations).hasKey(anyString());
verify(this.redisOperations).opsForHash();
verify(this.hashOperations).putAll(anyString(), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(3);
verify(this.redisOperations).expire(anyString(), any());
verifyZeroInteractions(this.redisOperations);
verifyZeroInteractions(this.hashOperations);
}
private Map<String, Object> map(Object... objects) {
Map<String, Object> result = new HashMap<>();
if (objects == null) {

View File

@@ -48,6 +48,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.events.AbstractSessionEvent;
@@ -146,7 +147,7 @@ class RedisOperationsSessionRepositoryTests {
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations);
RedisSession session = this.redisRepository.new RedisSession(this.cached);
RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
session.setLastAccessedTime(session.getLastAccessedTime());
String originalId = session.getId();
String changeSessionId = session.changeSessionId();
@@ -194,7 +195,7 @@ class RedisOperationsSessionRepositoryTests {
// gh-467
@Test
void saveSessionNothingChanged() {
RedisSession session = this.redisRepository.new RedisSession(this.cached);
RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
this.redisRepository.save(session);
@@ -230,7 +231,7 @@ class RedisOperationsSessionRepositoryTests {
@Test
void saveJavadoc() {
RedisSession session = this.redisRepository.new RedisSession(this.cached);
RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
session.setLastAccessedTime(session.getLastAccessedTime());
given(this.redisOperations.boundHashOps("spring:session:sessions:session-id"))
@@ -252,7 +253,7 @@ class RedisOperationsSessionRepositoryTests {
@Test
void saveLastAccessChanged() {
RedisSession session = this.redisRepository.new RedisSession(new MapSession(this.cached));
RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
session.setLastAccessedTime(Instant.ofEpochMilli(12345678L));
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -267,7 +268,7 @@ class RedisOperationsSessionRepositoryTests {
@Test
void saveSetAttribute() {
String attrName = "attrName";
RedisSession session = this.redisRepository.new RedisSession(new MapSession());
RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.setAttribute(attrName, "attrValue");
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -282,7 +283,7 @@ class RedisOperationsSessionRepositoryTests {
@Test
void saveRemoveAttribute() {
String attrName = "attrName";
RedisSession session = this.redisRepository.new RedisSession(new MapSession());
RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.removeAttribute(attrName);
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -295,7 +296,7 @@ class RedisOperationsSessionRepositoryTests {
@Test
void saveExpired() {
RedisSession session = this.redisRepository.new RedisSession(new MapSession());
RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.setMaxInactiveInterval(Duration.ZERO);
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -310,8 +311,7 @@ class RedisOperationsSessionRepositoryTests {
@Test
void redisSessionGetAttributes() {
String attrName = "attrName";
RedisSession session = this.redisRepository.new RedisSession(
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
RedisSession session = this.redisRepository.createSession();
assertThat(session.getAttributeNames()).isEmpty();
session.setAttribute(attrName, "attrValue");
assertThat(session.getAttributeNames()).containsOnly(attrName);
@@ -746,7 +746,7 @@ class RedisOperationsSessionRepositoryTests {
void changeRedisNamespace() {
String namespace = "foo:bar";
this.redisRepository.setRedisKeyNamespace(namespace);
RedisSession session = this.redisRepository.new RedisSession(new MapSession());
RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.setMaxInactiveInterval(Duration.ZERO);
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -772,7 +772,7 @@ class RedisOperationsSessionRepositoryTests {
@Test // gh-1120
void getAttributeNamesAndRemove() {
RedisSession session = this.redisRepository.new RedisSession(this.cached);
RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
session.setAttribute("attribute1", "value1");
session.setAttribute("attribute2", "value2");
@@ -836,6 +836,57 @@ class RedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.publisher);
}
@Test
void saveWithSaveModeOnSetAttribute() {
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations);
this.redisRepository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
RedisSession session = this.redisRepository.new RedisSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.redisRepository.save(session);
assertThat(getDelta()).hasSize(1);
}
@Test
void saveWithSaveModeOnGetAttribute() {
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations);
this.redisRepository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
RedisSession session = this.redisRepository.new RedisSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.redisRepository.save(session);
assertThat(getDelta()).hasSize(2);
}
@Test
void saveWithSaveModeAlways() {
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations);
this.redisRepository.setSaveMode(SaveMode.ALWAYS);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
RedisSession session = this.redisRepository.new RedisSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.redisRepository.save(session);
assertThat(getDelta()).hasSize(3);
}
private String getKey(String id) {
return "spring:session:sessions:" + id;
}

View File

@@ -35,6 +35,7 @@ import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils;
@@ -125,6 +126,19 @@ class SimpleRedisOperationsSessionRepositoryTests {
.withMessage("flushMode must not be null");
}
@Test
void setSaveMode_ValidSaveMode_ShouldSetSaveMode() {
this.sessionRepository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
assertThat(ReflectionTestUtils.getField(this.sessionRepository, "saveMode"))
.isEqualTo(SaveMode.ON_GET_ATTRIBUTE);
}
@Test
void setSaveMode_NullSaveMode_ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.sessionRepository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test
void createSession_DefaultMaxInactiveInterval_ShouldCreateSession() {
RedisSession redisSession = this.sessionRepository.createSession();
@@ -223,6 +237,69 @@ class SimpleRedisOperationsSessionRepositoryTests {
verifyNoMoreInteractions(this.sessionHashOperations);
}
@Test
void save_WithSaveModeOnSetAttribute_SholdSaveSession() {
given(this.sessionRedisOperations.hasKey(eq(TEST_SESSION_KEY))).willReturn(true);
this.sessionRepository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
Map<String, Object> attributes = new HashMap<>();
attributes.put("attribute1", "value1");
attributes.put("attribute2", "value2");
attributes.put("attribute3", "value3");
RedisSession session = createTestSession(attributes);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.sessionRepository.save(session);
verify(this.sessionRedisOperations).hasKey(eq(TEST_SESSION_KEY));
verify(this.sessionRedisOperations).opsForHash();
verify(this.sessionRedisOperations).expireAt(eq(TEST_SESSION_KEY), eq(getExpiry(session)));
verify(this.sessionHashOperations).putAll(eq(TEST_SESSION_KEY), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(1);
verifyNoMoreInteractions(this.sessionRedisOperations);
verifyNoMoreInteractions(this.sessionHashOperations);
}
@Test
void saveWithSaveModeOnGetAttribute() {
given(this.sessionRedisOperations.hasKey(eq(TEST_SESSION_KEY))).willReturn(true);
this.sessionRepository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
Map<String, Object> attributes = new HashMap<>();
attributes.put("attribute1", "value1");
attributes.put("attribute2", "value2");
attributes.put("attribute3", "value3");
RedisSession session = createTestSession(attributes);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.sessionRepository.save(session);
verify(this.sessionRedisOperations).hasKey(eq(TEST_SESSION_KEY));
verify(this.sessionRedisOperations).opsForHash();
verify(this.sessionRedisOperations).expireAt(eq(TEST_SESSION_KEY), eq(getExpiry(session)));
verify(this.sessionHashOperations).putAll(eq(TEST_SESSION_KEY), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(2);
verifyNoMoreInteractions(this.sessionRedisOperations);
verifyNoMoreInteractions(this.sessionHashOperations);
}
@Test
void saveWithSaveModeAlways() {
given(this.sessionRedisOperations.hasKey(eq(TEST_SESSION_KEY))).willReturn(true);
this.sessionRepository.setSaveMode(SaveMode.ALWAYS);
Map<String, Object> attributes = new HashMap<>();
attributes.put("attribute1", "value1");
attributes.put("attribute2", "value2");
attributes.put("attribute3", "value3");
RedisSession session = createTestSession(attributes);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.sessionRepository.save(session);
verify(this.sessionRedisOperations).hasKey(eq(TEST_SESSION_KEY));
verify(this.sessionRedisOperations).opsForHash();
verify(this.sessionRedisOperations).expireAt(eq(TEST_SESSION_KEY), eq(getExpiry(session)));
verify(this.sessionHashOperations).putAll(eq(TEST_SESSION_KEY), this.delta.capture());
assertThat(this.delta.getValue()).hasSize(3);
verifyNoMoreInteractions(this.sessionRedisOperations);
verifyNoMoreInteractions(this.sessionHashOperations);
}
@Test
void save_SessionNotExists_ShouldThrowException() {
RedisSession session = createTestSession();
@@ -315,12 +392,16 @@ class SimpleRedisOperationsSessionRepositoryTests {
return result;
}
private RedisSession createTestSession() {
private RedisSession createTestSession(Map<String, Object> attributes) {
MapSession cached = new MapSession(TEST_SESSION_ID);
cached.setCreationTime(Instant.EPOCH);
cached.setLastAccessedTime(Instant.EPOCH);
cached.setAttribute("attribute1", "value1");
return this.sessionRepository.new RedisSession(cached);
attributes.forEach(cached::setAttribute);
return this.sessionRepository.new RedisSession(cached, false);
}
private RedisSession createTestSession() {
return createTestSession(Collections.singletonMap("attribute1", "value1"));
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
@@ -142,6 +143,20 @@ class RedisHttpSessionConfigurationTests {
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(RedisOperationsSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
@@ -299,6 +314,20 @@ class RedisHttpSessionConfigurationTests {
}
@EnableRedisHttpSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomSaveModeExpressionSetterConfiguration extends RedisHttpSessionConfiguration {
CustomSaveModeExpressionSetterConfiguration() {
setSaveMode(SaveMode.ALWAYS);
}
}
@Configuration
@EnableRedisHttpSession
static class QualifiedConnectionFactoryRedisConfig {

View File

@@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
@@ -107,6 +108,20 @@ class RedisWebSessionConfigurationTests {
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(ReactiveRedisOperationsSessionRepository.class))
.hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS);
}
@Test
void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class);
@@ -249,6 +264,20 @@ class RedisWebSessionConfigurationTests {
}
@EnableRedisWebSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomSaveModeExpressionSetterConfiguration extends RedisWebSessionConfiguration {
CustomSaveModeExpressionSetterConfiguration() {
setSaveMode(SaveMode.ALWAYS);
}
}
@EnableRedisWebSession
static class QualifiedConnectionFactoryRedisConfig {