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

@@ -51,6 +51,7 @@ import org.springframework.session.FindByIndexNameSessionRepository;
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.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@@ -237,6 +238,8 @@ public class JdbcOperationsSessionRepository
private LobHandler lobHandler = new DefaultLobHandler();
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* Create a new {@link JdbcOperationsSessionRepository} instance which uses the
* provided {@link JdbcOperations} to manage sessions.
@@ -383,13 +386,22 @@ public class JdbcOperationsSessionRepository
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
public JdbcSession createSession() {
JdbcSession session = new JdbcSession();
MapSession delegate = new MapSession();
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
@@ -708,17 +720,13 @@ public class JdbcOperationsSessionRepository
private Map<String, DeltaValue> delta = new HashMap<>();
JdbcSession() {
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;
JdbcSession(MapSession delegate, String primaryKey, boolean isNew) {
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() {
@@ -757,7 +765,15 @@ public class JdbcOperationsSessionRepository
@Override
public <T> T getAttribute(String 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
@@ -848,7 +864,7 @@ public class JdbcOperationsSessionRepository
delegate.setCreationTime(Instant.ofEpochMilli(rs.getLong("CREATION_TIME")));
delegate.setLastAccessedTime(Instant.ofEpochMilli(rs.getLong("LAST_ACCESS_TIME")));
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");
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");
* 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.Import;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
@@ -96,4 +97,12 @@ public @interface EnableJdbcHttpSession {
*/
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.config.ScheduledTaskRegistrar;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
@@ -77,6 +78,8 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private DataSource dataSource;
private PlatformTransactionManager transactionManager;
@@ -100,6 +103,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
sessionRepository.setTableName(this.tableName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setSaveMode(this.saveMode);
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
}
@@ -142,6 +146,10 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
this.cleanupCron = cleanupCron;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@Autowired
public void setDataSource(@SpringSessionDataSource ObjectProvider<DataSource> springSessionDataSource,
ObjectProvider<DataSource> dataSource) {
@@ -199,6 +207,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
this.saveMode = attributes.getEnum("saveMode");
}
@Override

View File

@@ -22,6 +22,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
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.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.Session;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@@ -224,6 +227,12 @@ class JdbcOperationsSessionRepositoryTests {
.withMessage("conversionService must not be null");
}
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
.withMessage("saveMode must not be null");
}
@Test
void createSessionDefaultMaxInactiveInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
@@ -292,8 +301,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedAddSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue");
this.repository.save(session);
@@ -307,8 +316,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedAddMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
@@ -323,8 +332,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedModifySingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue");
session.clearChangeFlags();
session.setAttribute("testName", "testValue");
@@ -340,8 +349,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedModifyMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
session.clearChangeFlags();
@@ -359,8 +368,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedRemoveSingleAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue");
session.clearChangeFlags();
session.removeAttribute("testName");
@@ -376,8 +385,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedRemoveNonExistingAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.removeAttribute("testName");
this.repository.save(session);
@@ -389,8 +398,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedRemoveMultipleAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName1", "testValue1");
session.setAttribute("testName2", "testValue2");
session.clearChangeFlags();
@@ -408,8 +417,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070
void saveUpdatedAddAndModifyAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue1");
session.setAttribute("testName", "testValue2");
@@ -424,8 +433,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070
void saveUpdatedAddAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue");
session.removeAttribute("testName");
@@ -438,8 +447,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070
void saveUpdatedModifyAndRemoveAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue1");
session.clearChangeFlags();
session.setAttribute("testName", "testValue2");
@@ -456,8 +465,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test // gh-1070
void saveUpdatedRemoveAndAddAttribute() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setAttribute("testName", "testValue1");
session.clearChangeFlags();
session.removeAttribute("testName");
@@ -474,8 +483,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
@@ -489,8 +498,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUnchanged() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
this.repository.save(session);
@@ -516,7 +525,7 @@ class JdbcOperationsSessionRepositoryTests {
@Test
@SuppressWarnings("unchecked")
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));
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(expired));
@@ -533,7 +542,7 @@ class JdbcOperationsSessionRepositoryTests {
@Test
@SuppressWarnings("unchecked")
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");
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
isA(ResultSetExtractor.class))).willReturn(Collections.singletonList(saved));
@@ -592,10 +601,10 @@ class JdbcOperationsSessionRepositoryTests {
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused",
AuthorityUtils.createAuthorityList("ROLE_USER"));
List<Session> saved = new ArrayList<>(2);
Session saved1 = this.repository.new JdbcSession();
Session saved1 = this.repository.createSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1);
Session saved2 = this.repository.new JdbcSession();
Session saved2 = this.repository.createSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2);
given(this.jdbcOperations.query(isA(String.class), isA(PreparedStatementSetter.class),
@@ -647,8 +656,8 @@ class JdbcOperationsSessionRepositoryTests {
@Test
void saveUpdatedWithoutTransaction() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession("primaryKey",
new MapSession());
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession(),
"primaryKey", false);
session.setLastAccessedTime(Instant.now());
this.repository.save(session);
@@ -709,6 +718,61 @@ class JdbcOperationsSessionRepositoryTests {
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() {
ArgumentCaptor<TransactionDefinition> argument = ArgumentCaptor.forClass(TransactionDefinition.class);
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.support.lob.LobHandler;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.session.SaveMode;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
import org.springframework.test.util.ReflectionTestUtils;
@@ -135,6 +136,20 @@ class JdbcHttpSessionConfigurationTests {
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
void qualifiedDataSourceConfiguration() {
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
static class QualifiedDataSourceConfiguration {