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

@@ -45,6 +45,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.events.AbstractSessionEvent;
import org.springframework.session.events.SessionCreatedEvent;
@@ -147,6 +148,8 @@ public class HazelcastSessionRepository
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private IMap<String, MapSession> sessions;
private String sessionListenerId;
@@ -220,13 +223,24 @@ public class HazelcastSessionRepository
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
*/
public void setSaveMode(SaveMode saveMode) {
Assert.notNull(saveMode, "saveMode must not be null");
this.saveMode = saveMode;
}
@Override
public HazelcastSession createSession() {
HazelcastSession result = new HazelcastSession();
MapSession cached = new MapSession();
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
@@ -253,7 +267,7 @@ public class HazelcastSessionRepository
entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
}
if (!session.delta.isEmpty()) {
entryProcessor.setDelta(session.delta);
entryProcessor.setDelta(new HashMap<>(session.delta));
}
this.sessions.executeOnKey(session.getId(), entryProcessor);
}
@@ -274,7 +288,7 @@ public class HazelcastSessionRepository
deleteById(saved.getId());
return null;
}
return new HazelcastSession(saved);
return new HazelcastSession(saved, false);
}
@Override
@@ -290,7 +304,7 @@ public class HazelcastSessionRepository
Collection<MapSession> sessions = this.sessions.values(Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue));
Map<String, HazelcastSession> sessionMap = new HashMap<>(sessions.size());
for (MapSession session : sessions) {
sessionMap.put(session.getId(), new HazelcastSession(session));
sessionMap.put(session.getId(), new HazelcastSession(session, false));
}
return sessionMap;
}
@@ -347,25 +361,14 @@ public class HazelcastSessionRepository
private Map<String, Object> delta = new HashMap<>();
/**
* 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");
HazelcastSession(MapSession cached, boolean isNew) {
this.delegate = cached;
this.isNew = isNew;
this.originalId = cached.getId();
if (this.isNew || (HazelcastSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames()
.forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
}
}
@Override
@@ -416,7 +419,11 @@ public class HazelcastSessionRepository
@Override
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

View File

@@ -28,6 +28,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
@@ -113,4 +114,12 @@ public @interface EnableHazelcastHttpSession {
*/
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.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
@@ -56,6 +57,8 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private HazelcastInstance hazelcastInstance;
private ApplicationEventPublisher applicationEventPublisher;
@@ -69,6 +72,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
return sessionRepository;
}
@@ -89,6 +93,10 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
this.flushMode = flushMode;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@Autowired
public void setHazelcastInstance(
@SpringSessionHazelcastInstance ObjectProvider<HazelcastInstance> springSessionHazelcastInstance,
@@ -122,6 +130,7 @@ public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfigur
flushMode = hazelcastFlushMode.getFlushMode();
}
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
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.assertThatIllegalArgumentException;
@@ -83,6 +86,12 @@ class HazelcastSessionRepositoryTests {
.withMessage("HazelcastInstance must not be null");
}
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test
void createSessionDefaultMaxInactiveInterval() {
verify(this.sessions, times(1)).addEntryListener(any(MapListener.class), anyBoolean());
@@ -401,4 +410,61 @@ class HazelcastSessionRepositoryTests {
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.Primary;
import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.hazelcast.HazelcastFlushMode;
import org.springframework.session.hazelcast.HazelcastSessionRepository;
import org.springframework.session.hazelcast.config.annotation.SpringSessionHazelcastInstance;
@@ -149,6 +150,20 @@ class HazelcastHttpSessionConfigurationTests {
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
void qualifiedHazelcastInstanceConfiguration() {
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
@EnableHazelcastHttpSession(flushMode = FlushMode.IMMEDIATE)
static class CustomFlushImmediatelyConfiguration extends BaseConfiguration {