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

@@ -0,0 +1,49 @@
/*
* Copyright 2014-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session;
/**
* Supported modes of tracking and saving session changes to session store.
*
* @author Rob Winch
* @author Vedran Pavic
* @since 2.2.0
*/
public enum SaveMode {
/**
* Save only changes made to session, for instance using
* {@link Session#setAttribute(String, Object)}. In highly concurrent environments,
* this mode minimizes the risk of attributes being overwritten during processing of
* parallel requests.
*/
ON_SET_ATTRIBUTE,
/**
* Same as {@link #ON_SET_ATTRIBUTE} with addition of saving attributes that have been
* read using {@link Session#getAttribute(String)}.
*/
ON_GET_ATTRIBUTE,
/**
* Always save all session attributes, regardless of the interaction with the session.
* In highly concurrent environments, this mode increases the risk of attributes being
* overwritten during processing of parallel requests.
*/
ALWAYS
}

View File

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

View File

@@ -42,6 +42,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver; import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver; import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent; import org.springframework.session.events.SessionDeletedEvent;
@@ -296,6 +297,8 @@ public class RedisOperationsSessionRepository
private FlushMode flushMode = FlushMode.ON_SAVE; 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. * Creates a new instance. For an example, refer to the class level javadoc.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing the * @param sessionRedisOperations the {@link RedisOperations} to use for managing the
@@ -363,6 +366,15 @@ public class RedisOperationsSessionRepository
this.flushMode = flushMode; 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}. * Sets the database index to use. Defaults to {@link #DEFAULT_DATABASE}.
* @param database the database index to use * @param database the database index to use
@@ -439,7 +451,7 @@ public class RedisOperationsSessionRepository
if (!allowExpired && loaded.isExpired()) { if (!allowExpired && loaded.isExpired()) {
return null; return null;
} }
RedisSession result = new RedisSession(loaded); RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime(); result.originalLastAccessTime = loaded.getLastAccessedTime();
return result; return result;
} }
@@ -483,9 +495,11 @@ public class RedisOperationsSessionRepository
@Override @Override
public RedisSession createSession() { public RedisSession createSession() {
Duration maxInactiveInterval = Duration.ofSeconds((this.defaultMaxInactiveInterval != null) MapSession cached = new MapSession();
? this.defaultMaxInactiveInterval : MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); if (this.defaultMaxInactiveInterval != null) {
RedisSession session = new RedisSession(maxInactiveInterval); cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary(); session.flushImmediateIfNecessary();
return session; return session;
} }
@@ -674,32 +688,22 @@ public class RedisOperationsSessionRepository
private String originalSessionId; private String originalSessionId;
/** RedisSession(MapSession cached, boolean isNew) {
* 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");
this.cached = cached; this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId(); this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this); Map<String, String> indexes = RedisOperationsSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME); 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) { public void setNew(boolean isNew) {
@@ -709,7 +713,8 @@ public class RedisOperationsSessionRepository
@Override @Override
public void setLastAccessedTime(Instant lastAccessedTime) { public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(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 @Override
@@ -744,7 +749,8 @@ public class RedisOperationsSessionRepository
@Override @Override
public void setMaxInactiveInterval(Duration interval) { public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(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 @Override
@@ -754,7 +760,12 @@ public class RedisOperationsSessionRepository
@Override @Override
public <T> T getAttribute(String attributeName) { 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 @Override
@@ -765,13 +776,15 @@ public class RedisOperationsSessionRepository
@Override @Override
public void setAttribute(String attributeName, Object attributeValue) { public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue); this.cached.setAttribute(attributeName, attributeValue);
this.putAndFlush(getSessionAttrNameKey(attributeName), attributeValue); this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
} }
@Override @Override
public void removeAttribute(String attributeName) { public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName); this.cached.removeAttribute(attributeName);
this.putAndFlush(getSessionAttrNameKey(attributeName), null); this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
} }
private void 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() { private void save() {
saveChangeSessionId(); saveChangeSessionId();
saveDelta(); saveDelta();

View File

@@ -26,6 +26,7 @@ import java.util.Set;
import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisOperations;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.SessionRepository; import org.springframework.session.SessionRepository;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -52,6 +53,8 @@ public class SimpleRedisOperationsSessionRepository
private FlushMode flushMode = FlushMode.ON_SAVE; private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/** /**
* Create a new {@link SimpleRedisOperationsSessionRepository} instance. * Create a new {@link SimpleRedisOperationsSessionRepository} instance.
* @param sessionRedisOperations the {@link RedisOperations} to use for managing * @param sessionRedisOperations the {@link RedisOperations} to use for managing
@@ -89,9 +92,20 @@ public class SimpleRedisOperationsSessionRepository
this.flushMode = flushMode; 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 @Override
public RedisSession createSession() { 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(); session.flushIfRequired();
return session; return session;
} }
@@ -120,7 +134,7 @@ public class SimpleRedisOperationsSessionRepository
deleteById(sessionId); deleteById(sessionId);
return null; return null;
} }
return new RedisSession(session); return new RedisSession(session, false);
} }
@Override @Override
@@ -141,6 +155,10 @@ public class SimpleRedisOperationsSessionRepository
return this.keyNamespace + "sessions:" + sessionId; 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}. * An internal {@link Session} implementation used by this {@link SessionRepository}.
*/ */
@@ -154,18 +172,20 @@ public class SimpleRedisOperationsSessionRepository
private String originalSessionId; private String originalSessionId;
RedisSession(Duration maxInactiveInterval) { RedisSession(MapSession cached, boolean isNew) {
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) {
this.cached = cached; this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId(); 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 @Override
@@ -180,7 +200,12 @@ public class SimpleRedisOperationsSessionRepository
@Override @Override
public <T> T getAttribute(String attributeName) { 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 @Override
@@ -191,7 +216,8 @@ public class SimpleRedisOperationsSessionRepository
@Override @Override
public void setAttribute(String attributeName, Object attributeValue) { public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue); this.cached.setAttribute(attributeName, attributeValue);
putAttribute(RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName, attributeValue); this.delta.put(getAttributeKey(attributeName), attributeValue);
flushIfRequired();
} }
@Override @Override
@@ -207,7 +233,8 @@ public class SimpleRedisOperationsSessionRepository
@Override @Override
public void setLastAccessedTime(Instant lastAccessedTime) { public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(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 @Override
@@ -218,7 +245,8 @@ public class SimpleRedisOperationsSessionRepository
@Override @Override
public void setMaxInactiveInterval(Duration interval) { public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(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 @Override
@@ -274,11 +302,6 @@ public class SimpleRedisOperationsSessionRepository
this.delta.clear(); 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.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.SessionRepository; import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
@@ -119,4 +120,12 @@ public @interface EnableRedisHttpSession {
*/ */
String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; 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.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode; import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository; import org.springframework.session.data.redis.RedisOperationsSessionRepository;
@@ -86,6 +87,8 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
private FlushMode flushMode = FlushMode.ON_SAVE; private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private String cleanupCron = DEFAULT_CLEANUP_CRON; private String cleanupCron = DEFAULT_CLEANUP_CRON;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
@@ -117,6 +120,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
sessionRepository.setRedisKeyNamespace(this.redisNamespace); sessionRepository.setRedisKeyNamespace(this.redisNamespace);
} }
sessionRepository.setFlushMode(this.flushMode); sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase(); int database = resolveDatabase();
sessionRepository.setDatabase(database); sessionRepository.setDatabase(database);
return sessionRepository; return sessionRepository;
@@ -165,6 +169,10 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.flushMode = flushMode; this.flushMode = flushMode;
} }
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
public void setCleanupCron(String cleanupCron) { public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron; this.cleanupCron = cleanupCron;
} }
@@ -240,6 +248,7 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
flushMode = redisFlushMode.getFlushMode(); flushMode = redisFlushMode.getFlushMode();
} }
this.flushMode = flushMode; this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
String cleanupCron = attributes.getString("cleanupCron"); String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) { if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = 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.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository; import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
@@ -98,4 +99,12 @@ public @interface EnableRedisWebSession {
@Deprecated @Deprecated
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE; 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.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration; import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository; import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.RedisFlushMode; import org.springframework.session.data.redis.RedisFlushMode;
@@ -61,6 +62,8 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
private String redisNamespace = ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE; private String redisNamespace = ReactiveRedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private ReactiveRedisConnectionFactory redisConnectionFactory; private ReactiveRedisConnectionFactory redisConnectionFactory;
private RedisSerializer<Object> defaultRedisSerializer; private RedisSerializer<Object> defaultRedisSerializer;
@@ -78,6 +81,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
if (StringUtils.hasText(this.redisNamespace)) { if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace); sessionRepository.setRedisKeyNamespace(this.redisNamespace);
} }
sessionRepository.setSaveMode(this.saveMode);
return sessionRepository; return sessionRepository;
} }
@@ -94,6 +98,10 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null"); Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
} }
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@Autowired @Autowired
public void setRedisConnectionFactory( public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<ReactiveRedisConnectionFactory> springSessionRedisConnectionFactory, @SpringSessionRedisConnectionFactory ObjectProvider<ReactiveRedisConnectionFactory> springSessionRedisConnectionFactory,
@@ -132,6 +140,7 @@ public class RedisWebSessionConfiguration extends SpringWebSessionConfiguration
if (StringUtils.hasText(redisNamespaceValue)) { if (StringUtils.hasText(redisNamespaceValue)) {
this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue); this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue);
} }
this.saveMode = attributes.getEnum("saveMode");
} }
private ReactiveRedisTemplate<String, Object> createReactiveRedisTemplate() { 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.ReactiveHashOperations;
import org.springframework.data.redis.core.ReactiveRedisOperations; import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.RedisSession; import org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@@ -130,7 +131,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true)); given(this.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(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(); StepVerifier.create(this.repository.save(newSession)).verifyComplete();
verify(this.redisOperations).opsForHash(); verify(this.redisOperations).opsForHash();
@@ -154,7 +155,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true)); given(this.redisOperations.hasKey(anyString())).willReturn(Mono.just(true));
given(this.redisOperations.expire(anyString(), any())).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(); 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.hashOperations.putAll(anyString(), any())).willReturn(Mono.just(true));
given(this.redisOperations.expire(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)); session.setLastAccessedTime(Instant.ofEpochMilli(12345678L));
StepVerifier.create(this.repository.save(session)).verifyComplete(); StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -193,7 +194,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true)); given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
String attrName = "attrName"; String attrName = "attrName";
RedisSession session = this.repository.new RedisSession(this.cached); RedisSession session = this.repository.new RedisSession(this.cached, false);
session.setAttribute(attrName, "attrValue"); session.setAttribute(attrName, "attrValue");
StepVerifier.create(this.repository.save(session)).verifyComplete(); StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -216,7 +217,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true)); given(this.redisOperations.expire(anyString(), any())).willReturn(Mono.just(true));
String attrName = "attrName"; String attrName = "attrName";
RedisSession session = this.repository.new RedisSession(new MapSession()); RedisSession session = this.repository.new RedisSession(new MapSession(), false);
session.removeAttribute(attrName); session.removeAttribute(attrName);
StepVerifier.create(this.repository.save(session)).verifyComplete(); StepVerifier.create(this.repository.save(session)).verifyComplete();
@@ -234,7 +235,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@Test @Test
void redisSessionGetAttributes() { void redisSessionGetAttributes() {
String attrName = "attrName"; String attrName = "attrName";
RedisSession session = this.repository.new RedisSession(this.cached); RedisSession session = this.repository.new RedisSession(this.cached, false);
assertThat(session.getAttributeNames()).isEmpty(); assertThat(session.getAttributeNames()).isEmpty();
session.setAttribute(attrName, "attrValue"); session.setAttribute(attrName, "attrValue");
@@ -325,7 +326,7 @@ class ReactiveRedisOperationsSessionRepositoryTests {
@Test // gh-1120 @Test // gh-1120
void getAttributeNamesAndRemove() { 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("attribute1", "value1");
session.setAttribute("attribute2", "value2"); session.setAttribute("attribute2", "value2");
@@ -336,6 +337,78 @@ class ReactiveRedisOperationsSessionRepositoryTests {
assertThat(session.getAttributeNames()).isEmpty(); 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) { private Map<String, Object> map(Object... objects) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
if (objects == null) { 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.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession; import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession;
import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.AbstractSessionEvent;
@@ -146,7 +147,7 @@ class RedisOperationsSessionRepositoryTests {
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); 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()); session.setLastAccessedTime(session.getLastAccessedTime());
String originalId = session.getId(); String originalId = session.getId();
String changeSessionId = session.changeSessionId(); String changeSessionId = session.changeSessionId();
@@ -194,7 +195,7 @@ class RedisOperationsSessionRepositoryTests {
// gh-467 // gh-467
@Test @Test
void saveSessionNothingChanged() { void saveSessionNothingChanged() {
RedisSession session = this.redisRepository.new RedisSession(this.cached); RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
this.redisRepository.save(session); this.redisRepository.save(session);
@@ -230,7 +231,7 @@ class RedisOperationsSessionRepositoryTests {
@Test @Test
void saveJavadoc() { void saveJavadoc() {
RedisSession session = this.redisRepository.new RedisSession(this.cached); RedisSession session = this.redisRepository.new RedisSession(this.cached, false);
session.setLastAccessedTime(session.getLastAccessedTime()); session.setLastAccessedTime(session.getLastAccessedTime());
given(this.redisOperations.boundHashOps("spring:session:sessions:session-id")) given(this.redisOperations.boundHashOps("spring:session:sessions:session-id"))
@@ -252,7 +253,7 @@ class RedisOperationsSessionRepositoryTests {
@Test @Test
void saveLastAccessChanged() { 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)); session.setLastAccessedTime(Instant.ofEpochMilli(12345678L));
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -267,7 +268,7 @@ class RedisOperationsSessionRepositoryTests {
@Test @Test
void saveSetAttribute() { void saveSetAttribute() {
String attrName = "attrName"; String attrName = "attrName";
RedisSession session = this.redisRepository.new RedisSession(new MapSession()); RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.setAttribute(attrName, "attrValue"); session.setAttribute(attrName, "attrValue");
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -282,7 +283,7 @@ class RedisOperationsSessionRepositoryTests {
@Test @Test
void saveRemoveAttribute() { void saveRemoveAttribute() {
String attrName = "attrName"; String attrName = "attrName";
RedisSession session = this.redisRepository.new RedisSession(new MapSession()); RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.removeAttribute(attrName); session.removeAttribute(attrName);
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -295,7 +296,7 @@ class RedisOperationsSessionRepositoryTests {
@Test @Test
void saveExpired() { void saveExpired() {
RedisSession session = this.redisRepository.new RedisSession(new MapSession()); RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false);
session.setMaxInactiveInterval(Duration.ZERO); session.setMaxInactiveInterval(Duration.ZERO);
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -310,8 +311,7 @@ class RedisOperationsSessionRepositoryTests {
@Test @Test
void redisSessionGetAttributes() { void redisSessionGetAttributes() {
String attrName = "attrName"; String attrName = "attrName";
RedisSession session = this.redisRepository.new RedisSession( RedisSession session = this.redisRepository.createSession();
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
assertThat(session.getAttributeNames()).isEmpty(); assertThat(session.getAttributeNames()).isEmpty();
session.setAttribute(attrName, "attrValue"); session.setAttribute(attrName, "attrValue");
assertThat(session.getAttributeNames()).containsOnly(attrName); assertThat(session.getAttributeNames()).containsOnly(attrName);
@@ -746,7 +746,7 @@ class RedisOperationsSessionRepositoryTests {
void changeRedisNamespace() { void changeRedisNamespace() {
String namespace = "foo:bar"; String namespace = "foo:bar";
this.redisRepository.setRedisKeyNamespace(namespace); 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); session.setMaxInactiveInterval(Duration.ZERO);
given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations);
given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations);
@@ -772,7 +772,7 @@ class RedisOperationsSessionRepositoryTests {
@Test // gh-1120 @Test // gh-1120
void getAttributeNamesAndRemove() { 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("attribute1", "value1");
session.setAttribute("attribute2", "value2"); session.setAttribute("attribute2", "value2");
@@ -836,6 +836,57 @@ class RedisOperationsSessionRepositoryTests {
verifyZeroInteractions(this.publisher); 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) { private String getKey(String id) {
return "spring:session:sessions:" + 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.data.redis.core.RedisOperations;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession; import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@@ -125,6 +126,19 @@ class SimpleRedisOperationsSessionRepositoryTests {
.withMessage("flushMode must not be null"); .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 @Test
void createSession_DefaultMaxInactiveInterval_ShouldCreateSession() { void createSession_DefaultMaxInactiveInterval_ShouldCreateSession() {
RedisSession redisSession = this.sessionRepository.createSession(); RedisSession redisSession = this.sessionRepository.createSession();
@@ -223,6 +237,69 @@ class SimpleRedisOperationsSessionRepositoryTests {
verifyNoMoreInteractions(this.sessionHashOperations); 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 @Test
void save_SessionNotExists_ShouldThrowException() { void save_SessionNotExists_ShouldThrowException() {
RedisSession session = createTestSession(); RedisSession session = createTestSession();
@@ -315,12 +392,16 @@ class SimpleRedisOperationsSessionRepositoryTests {
return result; return result;
} }
private RedisSession createTestSession() { private RedisSession createTestSession(Map<String, Object> attributes) {
MapSession cached = new MapSession(TEST_SESSION_ID); MapSession cached = new MapSession(TEST_SESSION_ID);
cached.setCreationTime(Instant.EPOCH); cached.setCreationTime(Instant.EPOCH);
cached.setLastAccessedTime(Instant.EPOCH); cached.setLastAccessedTime(Instant.EPOCH);
cached.setAttribute("attribute1", "value1"); attributes.forEach(cached::setAttribute);
return this.sessionRepository.new RedisSession(cached); 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.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.RedisFlushMode; import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository; import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
@@ -142,6 +143,20 @@ class RedisHttpSessionConfigurationTests {
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); 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 @Test
void qualifiedConnectionFactoryRedisConfig() { void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class); 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 @Configuration
@EnableRedisHttpSession @EnableRedisHttpSession
static class QualifiedConnectionFactoryRedisConfig { 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.core.ReactiveRedisOperations;
import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer; 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.ReactiveRedisOperationsSessionRepository;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations;
@@ -107,6 +108,20 @@ class RedisWebSessionConfigurationTests {
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS); .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 @Test
void qualifiedConnectionFactoryRedisConfig() { void qualifiedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class); 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 @EnableRedisWebSession
static class QualifiedConnectionFactoryRedisConfig { static class QualifiedConnectionFactoryRedisConfig {

View File

@@ -45,6 +45,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver; import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver; import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionCreatedEvent;
@@ -147,6 +148,8 @@ public class HazelcastSessionRepository
private FlushMode flushMode = FlushMode.ON_SAVE; private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private IMap<String, MapSession> sessions; private IMap<String, MapSession> sessions;
private String sessionListenerId; private String sessionListenerId;
@@ -220,13 +223,24 @@ public class HazelcastSessionRepository
this.flushMode = flushMode; 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 @Override
public HazelcastSession createSession() { public HazelcastSession createSession() {
HazelcastSession result = new HazelcastSession(); MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) { if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval)); cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
} }
return result; HazelcastSession session = new HazelcastSession(cached, true);
session.flushImmediateIfNecessary();
return session;
} }
@Override @Override
@@ -253,7 +267,7 @@ public class HazelcastSessionRepository
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval()); entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
} }
if (!session.delta.isEmpty()) { if (!session.delta.isEmpty()) {
entryProcessor.setDelta(session.delta); entryProcessor.setDelta(new HashMap<>(session.delta));
} }
this.sessions.executeOnKey(session.getId(), entryProcessor); this.sessions.executeOnKey(session.getId(), entryProcessor);
} }
@@ -274,7 +288,7 @@ public class HazelcastSessionRepository
deleteById(saved.getId()); deleteById(saved.getId());
return null; return null;
} }
return new HazelcastSession(saved); return new HazelcastSession(saved, false);
} }
@Override @Override
@@ -290,7 +304,7 @@ public class HazelcastSessionRepository
Collection<MapSession> sessions = this.sessions.values(Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue)); Collection<MapSession> sessions = this.sessions.values(Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue));
Map<String, HazelcastSession> sessionMap = new HashMap<>(sessions.size()); Map<String, HazelcastSession> sessionMap = new HashMap<>(sessions.size());
for (MapSession session : sessions) { for (MapSession session : sessions) {
sessionMap.put(session.getId(), new HazelcastSession(session)); sessionMap.put(session.getId(), new HazelcastSession(session, false));
} }
return sessionMap; return sessionMap;
} }
@@ -347,25 +361,14 @@ public class HazelcastSessionRepository
private Map<String, Object> delta = new HashMap<>(); private Map<String, Object> delta = new HashMap<>();
/** HazelcastSession(MapSession cached, boolean isNew) {
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
*/
HazelcastSession() {
this(new MapSession());
this.isNew = true;
flushImmediateIfNecessary();
}
/**
* Creates a new instance from the provided {@link MapSession}.
* @param cached the {@link MapSession} that represents the persisted session that
* was retrieved. Cannot be {@code null}.
*/
HazelcastSession(MapSession cached) {
Assert.notNull(cached, "MapSession cannot be null");
this.delegate = cached; this.delegate = cached;
this.isNew = isNew;
this.originalId = cached.getId(); this.originalId = cached.getId();
if (this.isNew || (HazelcastSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames()
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
}
} }
@Override @Override
@@ -416,7 +419,11 @@ public class HazelcastSessionRepository
@Override @Override
public <T> T getAttribute(String attributeName) { public <T> T getAttribute(String attributeName) {
return this.delegate.getAttribute(attributeName); T attributeValue = this.delegate.getAttribute(attributeName);
if (attributeValue != null && HazelcastSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, attributeValue);
}
return attributeValue;
} }
@Override @Override

View File

@@ -28,6 +28,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.session.SessionRepository; import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
@@ -113,4 +114,12 @@ public @interface EnableHazelcastHttpSession {
*/ */
FlushMode flushMode() default FlushMode.ON_SAVE; FlushMode flushMode() default FlushMode.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

@@ -30,6 +30,7 @@ import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.hazelcast.HazelcastFlushMode; import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository; import org.springframework.session.hazelcast.HazelcastSessionRepository;
@@ -56,6 +57,8 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
private FlushMode flushMode = FlushMode.ON_SAVE; private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private HazelcastInstance hazelcastInstance; private HazelcastInstance hazelcastInstance;
private ApplicationEventPublisher applicationEventPublisher; private ApplicationEventPublisher applicationEventPublisher;
@@ -69,6 +72,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
} }
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode); sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
return sessionRepository; return sessionRepository;
} }
@@ -89,6 +93,10 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
this.flushMode = flushMode; this.flushMode = flushMode;
} }
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@Autowired @Autowired
public void setHazelcastInstance( public void setHazelcastInstance(
@SpringSessionHazelcastInstance ObjectProvider<HazelcastInstance> springSessionHazelcastInstance, @SpringSessionHazelcastInstance ObjectProvider<HazelcastInstance> springSessionHazelcastInstance,
@@ -122,6 +130,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
flushMode = hazelcastFlushMode.getFlushMode(); flushMode = hazelcastFlushMode.getFlushMode();
} }
this.flushMode = flushMode; this.flushMode = flushMode;
this.saveMode = attributes.getEnum("saveMode");
} }
} }

View File

@@ -30,6 +30,7 @@ import com.hazelcast.map.listener.MapListener;
import com.hazelcast.query.impl.predicates.EqualPredicate; import com.hazelcast.query.impl.predicates.EqualPredicate;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -37,7 +38,9 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession; import org.springframework.session.hazelcast.HazelcastSessionRepository.HazelcastSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -83,6 +86,12 @@ class HazelcastSessionRepositoryTests {
.withMessage("HazelcastInstance must not be null"); .withMessage("HazelcastInstance must not be null");
} }
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test @Test
void createSessionDefaultMaxInactiveInterval() { void createSessionDefaultMaxInactiveInterval() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean()); verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
@@ -401,4 +410,61 @@ class HazelcastSessionRepositoryTests {
assertThat(session.getAttributeNames()).isEmpty(); assertThat(session.getAttributeNames()).isEmpty();
} }
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeOnSetAttribute() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<SessionUpdateEntryProcessor> captor = ArgumentCaptor.forClass(SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(1);
verifyZeroInteractions(this.sessions);
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeOnGetAttribute() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<SessionUpdateEntryProcessor> captor = ArgumentCaptor.forClass(SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(2);
verifyZeroInteractions(this.sessions);
}
@Test
@SuppressWarnings("unchecked")
void saveWithSaveModeAlways() {
verify(this.sessions).addEntryListener(any(MapListener.class), anyBoolean());
this.repository.setSaveMode(SaveMode.ALWAYS);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", "value1");
delegate.setAttribute("attribute2", "value2");
delegate.setAttribute("attribute3", "value3");
HazelcastSession session = this.repository.new HazelcastSession(delegate, false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<SessionUpdateEntryProcessor> captor = ArgumentCaptor.forClass(SessionUpdateEntryProcessor.class);
verify(this.sessions).executeOnKey(eq(session.getId()), captor.capture());
assertThat((Map<String, Object>) ReflectionTestUtils.getField(captor.getValue(), "delta")).hasSize(3);
verifyZeroInteractions(this.sessions);
}
} }

View File

@@ -27,6 +27,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.session.FlushMode; import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.hazelcast.HazelcastFlushMode; import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository; import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance; import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
@@ -149,6 +150,20 @@ class HazelcastHttpSessionConfigurationTests {
assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); assertThat(ReflectionTestUtils.getField(repository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE);
} }
@Test
void customSaveModeAnnotation() {
registerAndRefresh(BaseConfiguration.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(HazelcastSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(BaseConfiguration.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(HazelcastSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test @Test
void qualifiedHazelcastInstanceConfiguration() { void qualifiedHazelcastInstanceConfiguration() {
registerAndRefresh(QualifiedHazelcastInstanceConfiguration.class); registerAndRefresh(QualifiedHazelcastInstanceConfiguration.class);
@@ -285,6 +300,20 @@ class HazelcastHttpSessionConfigurationTests {
} }
@EnableHazelcastHttpSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomSaveModeExpressionSetterConfiguration extends HazelcastHttpSessionConfiguration {
CustomSaveModeExpressionSetterConfiguration() {
setSaveMode(SaveMode.ALWAYS);
}
}
@Configuration @Configuration
@EnableHazelcastHttpSession(flushMode = FlushMode.IMMEDIATE) @EnableHazelcastHttpSession(flushMode = FlushMode.IMMEDIATE)
static class CustomFlushImmediatelyConfiguration extends BaseConfiguration { static class CustomFlushImmediatelyConfiguration extends BaseConfiguration {

View File

@@ -51,6 +51,7 @@ import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.IndexResolver; import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver; import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
@@ -237,6 +238,8 @@ public class JdbcOperationsSessionRepository
private LobHandler lobHandler = new DefaultLobHandler(); private LobHandler lobHandler = new DefaultLobHandler();
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/** /**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the * Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided {@link JdbcOperations} to manage sessions. * provided {@link JdbcOperations} to manage sessions.
@@ -383,13 +386,22 @@ public class JdbcOperationsSessionRepository
this.conversionService = conversionService; this.conversionService = conversionService;
} }
/**
* 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 @Override
public JdbcSession createSession() { public JdbcSession createSession() {
JdbcSession session = new JdbcSession(); MapSession delegate = new MapSession();
if (this.defaultMaxInactiveInterval != null) { if (this.defaultMaxInactiveInterval != null) {
session.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval)); delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
} }
return session; return new JdbcSession(delegate, UUID.randomUUID().toString(), true);
} }
@Override @Override
@@ -708,17 +720,13 @@ public class JdbcOperationsSessionRepository
private Map<String, DeltaValue> delta = new HashMap<>(); private Map<String, DeltaValue> delta = new HashMap<>();
JdbcSession() { JdbcSession(MapSession delegate, String primaryKey, boolean isNew) {
this.delegate = new MapSession();
this.isNew = true;
this.primaryKey = UUID.randomUUID().toString();
}
JdbcSession(String primaryKey, Session delegate) {
Assert.notNull(primaryKey, "primaryKey cannot be null");
Assert.notNull(delegate, "Session cannot be null");
this.primaryKey = primaryKey;
this.delegate = delegate; this.delegate = delegate;
this.primaryKey = primaryKey;
this.isNew = isNew;
if (this.isNew || (JdbcOperationsSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(attributeName, DeltaValue.UPDATED));
}
} }
boolean isNew() { boolean isNew() {
@@ -757,7 +765,15 @@ public class JdbcOperationsSessionRepository
@Override @Override
public <T> T getAttribute(String attributeName) { public <T> T getAttribute(String attributeName) {
Supplier<T> supplier = this.delegate.getAttribute(attributeName); Supplier<T> supplier = this.delegate.getAttribute(attributeName);
return (supplier != null) ? supplier.get() : null; if (supplier == null) {
return null;
}
T attributeValue = supplier.get();
if (attributeValue != null
&& JdbcOperationsSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(attributeName, DeltaValue.UPDATED);
}
return attributeValue;
} }
@Override @Override
@@ -848,7 +864,7 @@ public class JdbcOperationsSessionRepository
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME"))); delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME"))); delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL"))); delegate.setMaxInactiveInterval(Duration.ofSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")));
session = new JdbcSession(primaryKey, delegate); session = new JdbcSession(delegate, primaryKey, false);
} }
String attributeName = rs.getString("ATTRIBUTE_NAME"); String attributeName = rs.getString("ATTRIBUTE_NAME");
if (attributeName != null) { if (attributeName != null) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2017 the original author or authors. * Copyright 2014-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import javax.sql.DataSource;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter; import org.springframework.session.web.http.SessionRepositoryFilter;
@@ -96,4 +97,12 @@ public @interface EnableJdbcHttpSession {
*/ */
String cleanupCron() default JdbcHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; String cleanupCron() default JdbcHttpSessionConfiguration.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

@@ -43,6 +43,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource; import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
@@ -77,6 +78,8 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
private String cleanupCron = DEFAULT_CLEANUP_CRON; private String cleanupCron = DEFAULT_CLEANUP_CRON;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private DataSource dataSource; private DataSource dataSource;
private PlatformTransactionManager transactionManager; private PlatformTransactionManager transactionManager;
@@ -100,6 +103,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
sessionRepository.setTableName(this.tableName); sessionRepository.setTableName(this.tableName);
} }
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setSaveMode(this.saveMode);
if (this.lobHandler != null) { if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler); sessionRepository.setLobHandler(this.lobHandler);
} }
@@ -142,6 +146,10 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
this.cleanupCron = cleanupCron; this.cleanupCron = cleanupCron;
} }
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@Autowired @Autowired
public void setDataSource(@SpringSessionDataSource ObjectProvider<DataSource> springSessionDataSource, public void setDataSource(@SpringSessionDataSource ObjectProvider<DataSource> springSessionDataSource,
ObjectProvider<DataSource> dataSource) { ObjectProvider<DataSource> dataSource) {
@@ -199,6 +207,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
if (StringUtils.hasText(cleanupCron)) { if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron; this.cleanupCron = cleanupCron;
} }
this.saveMode = attributes.getEnum("saveMode");
} }
@Override @Override

View File

@@ -22,6 +22,8 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -36,6 +38,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession; import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session; import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
@@ -224,6 +227,12 @@ class JdbcOperationsSessionRepositoryTests {
.withMessage("conversionService must not be null"); .withMessage("conversionService must not be null");
} }
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test @Test
void createSessionDefaultMaxInactiveInterval() { void createSessionDefaultMaxInactiveInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession(); JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
@@ -292,8 +301,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedAddSingleAttribute() { void saveUpdatedAddSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue"); session.setAttribute("testName", "testValue");
this.repository.save(session); this.repository.save(session);
@@ -307,8 +316,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedAddMultipleAttributes() { void saveUpdatedAddMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName1", "testValue1"); session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2"); session.setAttribute("testName2", "testValue2");
@@ -323,8 +332,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedModifySingleAttribute() { void saveUpdatedModifySingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue"); session.setAttribute("testName", "testValue");
session.clearChangeFlags(); session.clearChangeFlags();
session.setAttribute("testName", "testValue"); session.setAttribute("testName", "testValue");
@@ -340,8 +349,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedModifyMultipleAttributes() { void saveUpdatedModifyMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName1", "testValue1"); session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2"); session.setAttribute("testName2", "testValue2");
session.clearChangeFlags(); session.clearChangeFlags();
@@ -359,8 +368,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedRemoveSingleAttribute() { void saveUpdatedRemoveSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue"); session.setAttribute("testName", "testValue");
session.clearChangeFlags(); session.clearChangeFlags();
session.removeAttribute("testName"); session.removeAttribute("testName");
@@ -376,8 +385,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedRemoveNonExistingAttribute() { void saveUpdatedRemoveNonExistingAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.removeAttribute("testName"); session.removeAttribute("testName");
this.repository.save(session); this.repository.save(session);
@@ -389,8 +398,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedRemoveMultipleAttributes() { void saveUpdatedRemoveMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName1", "testValue1"); session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2"); session.setAttribute("testName2", "testValue2");
session.clearChangeFlags(); session.clearChangeFlags();
@@ -408,8 +417,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070 @Test // gh-1070
void saveUpdatedAddAndModifyAttribute() { void saveUpdatedAddAndModifyAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue1"); session.setAttribute("testName", "testValue1");
session.setAttribute("testName", "testValue2"); session.setAttribute("testName", "testValue2");
@@ -424,8 +433,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070 @Test // gh-1070
void saveUpdatedAddAndRemoveAttribute() { void saveUpdatedAddAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue"); session.setAttribute("testName", "testValue");
session.removeAttribute("testName"); session.removeAttribute("testName");
@@ -438,8 +447,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070 @Test // gh-1070
void saveUpdatedModifyAndRemoveAttribute() { void saveUpdatedModifyAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue1"); session.setAttribute("testName", "testValue1");
session.clearChangeFlags(); session.clearChangeFlags();
session.setAttribute("testName", "testValue2"); session.setAttribute("testName", "testValue2");
@@ -456,8 +465,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070 @Test // gh-1070
void saveUpdatedRemoveAndAddAttribute() { void saveUpdatedRemoveAndAddAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setAttribute("testName", "testValue1"); session.setAttribute("testName", "testValue1");
session.clearChangeFlags(); session.clearChangeFlags();
session.removeAttribute("testName"); session.removeAttribute("testName");
@@ -474,8 +483,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedLastAccessedTime() { void saveUpdatedLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setLastAccessedTime(Instant.now()); session.setLastAccessedTime(Instant.now());
this.repository.save(session); this.repository.save(session);
@@ -489,8 +498,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUnchanged() { void saveUnchanged() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
this.repository.save(session); this.repository.save(session);
@@ -516,7 +525,7 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void getSessionExpired() { void getSessionExpired() {
Session expired = this.repository.new JdbcSession(); Session expired = this.repository.createSession();
expired.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1)); expired.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class), given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(expired)); isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(expired));
@@ -533,7 +542,7 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void getSessionFound() { void getSessionFound() {
Session saved = this.repository.new JdbcSession("primaryKey", new MapSession()); Session saved = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
saved.setAttribute("savedName", "savedValue"); saved.setAttribute("savedName", "savedValue");
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class), given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(saved)); isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(saved));
@@ -592,10 +601,10 @@ class JdbcOperationsSessionRepositoryTests {
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused", Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused",
AuthorityUtils.createAuthorityList("ROLE_USER")); AuthorityUtils.createAuthorityList("ROLE_USER"));
List<Session> saved = new ArrayList<>(2); List<Session> saved = new ArrayList<>(2);
Session saved1 = this.repository.new JdbcSession(); Session saved1 = this.repository.createSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication); saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1); saved.add(saved1);
Session saved2 = this.repository.new JdbcSession(); Session saved2 = this.repository.createSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication); saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2); saved.add(saved2);
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class), given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
@@ -647,8 +656,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test @Test
void saveUpdatedWithoutTransaction() { void saveUpdatedWithoutTransaction() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations); this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey", JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
new MapSession()); "primaryKey", false);
session.setLastAccessedTime(Instant.now()); session.setLastAccessedTime(Instant.now());
this.repository.save(session); this.repository.save(session);
@@ -709,6 +718,61 @@ class JdbcOperationsSessionRepositoryTests {
verifyZeroInteractions(this.transactionManager); verifyZeroInteractions(this.transactionManager);
} }
@Test
void saveWithSaveModeOnSetAttribute() {
this.repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
delegate.setAttribute("attribute2", (Supplier<String>) () -> "value2");
delegate.setAttribute("attribute3", (Supplier<String>) () -> "value3");
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(delegate,
UUID.randomUUID().toString(), false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"),
isA(PreparedStatementSetter.class));
verifyZeroInteractions(this.jdbcOperations);
}
@Test
void saveWithSaveModeOnGetAttribute() {
this.repository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
delegate.setAttribute("attribute2", (Supplier<String>) () -> "value2");
delegate.setAttribute("attribute3", (Supplier<String>) () -> "value3");
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(delegate,
UUID.randomUUID().toString(), false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<BatchPreparedStatementSetter> captor = ArgumentCaptor
.forClass(BatchPreparedStatementSetter.class);
verify(this.jdbcOperations).batchUpdate(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"), captor.capture());
assertThat(captor.getValue().getBatchSize()).isEqualTo(2);
verifyZeroInteractions(this.jdbcOperations);
}
@Test
void saveWithSaveModeAlways() {
this.repository.setSaveMode(SaveMode.ALWAYS);
MapSession delegate = new MapSession();
delegate.setAttribute("attribute1", (Supplier<String>) () -> "value1");
delegate.setAttribute("attribute2", (Supplier<String>) () -> "value2");
delegate.setAttribute("attribute3", (Supplier<String>) () -> "value3");
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(delegate,
UUID.randomUUID().toString(), false);
session.getAttribute("attribute2");
session.setAttribute("attribute3", "value4");
this.repository.save(session);
ArgumentCaptor<BatchPreparedStatementSetter> captor = ArgumentCaptor
.forClass(BatchPreparedStatementSetter.class);
verify(this.jdbcOperations).batchUpdate(startsWith("UPDATE SPRING_SESSION_ATTRIBUTES SET"), captor.capture());
assertThat(captor.getValue().getBatchSize()).isEqualTo(3);
verifyZeroInteractions(this.jdbcOperations);
}
private void assertPropagationRequiresNew() { private void assertPropagationRequiresNew() {
ArgumentCaptor<TransactionDefinition> argument = ArgumentCaptor.forClass(TransactionDefinition.class); ArgumentCaptor<TransactionDefinition> argument = ArgumentCaptor.forClass(TransactionDefinition.class);
verify(this.transactionManager, atLeastOnce()).getTransaction(argument.capture()); verify(this.transactionManager, atLeastOnce()).getTransaction(argument.capture());

View File

@@ -31,6 +31,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.support.lob.LobHandler; import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.SaveMode;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource; import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@@ -135,6 +136,20 @@ class JdbcHttpSessionConfigurationTests {
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
} }
@Test
void customSaveModeAnnotation() {
registerAndRefresh(DataSourceConfiguration.class, CustomSaveModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(JdbcHttpSessionConfiguration.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test
void customSaveModeSetter() {
registerAndRefresh(DataSourceConfiguration.class, CustomSaveModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(JdbcHttpSessionConfiguration.class)).hasFieldOrPropertyWithValue("saveMode",
SaveMode.ALWAYS);
}
@Test @Test
void qualifiedDataSourceConfiguration() { void qualifiedDataSourceConfiguration() {
registerAndRefresh(DataSourceConfiguration.class, QualifiedDataSourceConfiguration.class); registerAndRefresh(DataSourceConfiguration.class, QualifiedDataSourceConfiguration.class);
@@ -300,6 +315,20 @@ class JdbcHttpSessionConfigurationTests {
} }
@EnableJdbcHttpSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomSaveModeExpressionSetterConfiguration extends JdbcHttpSessionConfiguration {
CustomSaveModeExpressionSetterConfiguration() {
setSaveMode(SaveMode.ALWAYS);
}
}
@EnableJdbcHttpSession @EnableJdbcHttpSession
static class QualifiedDataSourceConfiguration { static class QualifiedDataSourceConfiguration {