Add flush mode support for JDBC sessions

Resolves: #1468
This commit is contained in:
Vedran Pavic
2019-07-26 23:58:15 +02:00
parent 23bf92a086
commit dbeb33fd9d
5 changed files with 213 additions and 67 deletions

View File

@@ -48,6 +48,7 @@ import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.IndexResolver;
import org.springframework.session.MapSession;
import org.springframework.session.PrincipalNameIndexResolver;
@@ -238,6 +239,8 @@ public class JdbcOperationsSessionRepository
private LobHandler lobHandler = new DefaultLobHandler();
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
@@ -386,6 +389,15 @@ public class JdbcOperationsSessionRepository
this.conversionService = conversionService;
}
/**
* Set the flush mode. Default is {@link FlushMode#ON_SAVE}.
* @param flushMode the flush mode
*/
public void setFlushMode(FlushMode flushMode) {
Assert.notNull(flushMode, "flushMode must not be null");
this.flushMode = flushMode;
}
/**
* Set the save mode.
* @param saveMode the save mode
@@ -401,77 +413,14 @@ public class JdbcOperationsSessionRepository
if (this.defaultMaxInactiveInterval != null) {
delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return new JdbcSession(delegate, UUID.randomUUID().toString(), true);
JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
session.flushIfRequired();
return session;
}
@Override
public void save(final JdbcSession session) {
if (session.isNew()) {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(session);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.createSessionQuery, (ps) -> {
ps.setString(1, session.primaryKey);
ps.setString(2, session.getId());
ps.setLong(3, session.getCreationTime().toEpochMilli());
ps.setLong(4, session.getLastAccessedTime().toEpochMilli());
ps.setInt(5, (int) session.getMaxInactiveInterval().getSeconds());
ps.setLong(6, session.getExpiryTime().toEpochMilli());
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
});
Set<String> attributeNames = session.getAttributeNames();
if (!attributeNames.isEmpty()) {
insertSessionAttributes(session, new ArrayList<>(attributeNames));
}
}
});
}
else {
this.transactionOperations.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (session.isChanged()) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(session);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.updateSessionQuery, (ps) -> {
ps.setString(1, session.getId());
ps.setLong(2, session.getLastAccessedTime().toEpochMilli());
ps.setInt(3, (int) session.getMaxInactiveInterval().getSeconds());
ps.setLong(4, session.getExpiryTime().toEpochMilli());
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
ps.setString(6, session.primaryKey);
});
}
List<String> addedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.ADDED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!addedAttributeNames.isEmpty()) {
insertSessionAttributes(session, addedAttributeNames);
}
List<String> updatedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!updatedAttributeNames.isEmpty()) {
updateSessionAttributes(session, updatedAttributeNames);
}
List<String> removedAttributeNames = session.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!removedAttributeNames.isEmpty()) {
deleteSessionAttributes(session, removedAttributeNames);
}
}
});
}
session.clearChangeFlags();
session.save();
}
@Override
@@ -806,6 +755,7 @@ public class JdbcOperationsSessionRepository
if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) || SPRING_SECURITY_CONTEXT.equals(attributeName)) {
this.changed = true;
}
flushIfRequired();
}
@Override
@@ -822,6 +772,7 @@ public class JdbcOperationsSessionRepository
public void setLastAccessedTime(Instant lastAccessedTime) {
this.delegate.setLastAccessedTime(lastAccessedTime);
this.changed = true;
flushIfRequired();
}
@Override
@@ -833,6 +784,7 @@ public class JdbcOperationsSessionRepository
public void setMaxInactiveInterval(Duration interval) {
this.delegate.setMaxInactiveInterval(interval);
this.changed = true;
flushIfRequired();
}
@Override
@@ -845,6 +797,83 @@ public class JdbcOperationsSessionRepository
return this.delegate.isExpired();
}
private void flushIfRequired() {
if (JdbcOperationsSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
if (this.isNew) {
JdbcOperationsSessionRepository.this.transactionOperations
.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(JdbcSession.this);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.createSessionQuery, (ps) -> {
ps.setString(1, JdbcSession.this.primaryKey);
ps.setString(2, getId());
ps.setLong(3, getCreationTime().toEpochMilli());
ps.setLong(4, getLastAccessedTime().toEpochMilli());
ps.setInt(5, (int) getMaxInactiveInterval().getSeconds());
ps.setLong(6, getExpiryTime().toEpochMilli());
ps.setString(7, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
});
Set<String> attributeNames = getAttributeNames();
if (!attributeNames.isEmpty()) {
insertSessionAttributes(JdbcSession.this, new ArrayList<>(attributeNames));
}
}
});
}
else {
JdbcOperationsSessionRepository.this.transactionOperations
.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (JdbcSession.this.changed) {
Map<String, String> indexes = JdbcOperationsSessionRepository.this.indexResolver
.resolveIndexesFor(JdbcSession.this);
JdbcOperationsSessionRepository.this.jdbcOperations
.update(JdbcOperationsSessionRepository.this.updateSessionQuery, (ps) -> {
ps.setString(1, getId());
ps.setLong(2, getLastAccessedTime().toEpochMilli());
ps.setInt(3, (int) getMaxInactiveInterval().getSeconds());
ps.setLong(4, getExpiryTime().toEpochMilli());
ps.setString(5, indexes.get(PRINCIPAL_NAME_INDEX_NAME));
ps.setString(6, JdbcSession.this.primaryKey);
});
}
List<String> addedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.ADDED).map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!addedAttributeNames.isEmpty()) {
insertSessionAttributes(JdbcSession.this, addedAttributeNames);
}
List<String> updatedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.UPDATED)
.map(Map.Entry::getKey).collect(Collectors.toList());
if (!updatedAttributeNames.isEmpty()) {
updateSessionAttributes(JdbcSession.this, updatedAttributeNames);
}
List<String> removedAttributeNames = JdbcSession.this.delta.entrySet().stream()
.filter((entry) -> entry.getValue() == DeltaValue.REMOVED)
.map(Map.Entry::getKey).collect(Collectors.toList());
if (!removedAttributeNames.isEmpty()) {
deleteSessionAttributes(JdbcSession.this, removedAttributeNames);
}
}
});
}
clearChangeFlags();
}
}
private class SessionResultSetExtractor implements ResultSetExtractor<List<JdbcSession>> {

View File

@@ -26,8 +26,11 @@ import javax.sql.DataSource;
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;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
@@ -97,6 +100,18 @@ public @interface EnableJdbcHttpSession {
*/
String cleanupCron() default JdbcHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
/**
* Flush mode for the sessions. The default is {@code ON_SAVE} which only updates the
* backing database when {@link SessionRepository#save(Session)} is invoked. In a web
* environment this happens just before the HTTP response is committed.
* <p>
* Setting the value to {@code IMMEDIATE} will ensure that the any updates to the
* Session are immediately written to the database.
* @return the flush mode
* @since 2.2.0
*/
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.

View File

@@ -42,6 +42,7 @@ import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.session.FlushMode;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
@@ -78,6 +79,8 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
private String cleanupCron = DEFAULT_CLEANUP_CRON;
private FlushMode flushMode = FlushMode.ON_SAVE;
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
private DataSource dataSource;
@@ -103,6 +106,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
sessionRepository.setTableName(this.tableName);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
if (this.lobHandler != null) {
sessionRepository.setLobHandler(this.lobHandler);
@@ -146,6 +150,10 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
this.cleanupCron = cleanupCron;
}
public void setFlushMode(FlushMode flushMode) {
this.flushMode = flushMode;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
@@ -207,6 +215,7 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
this.flushMode = attributes.getEnum("flushMode");
this.saveMode = attributes.getEnum("saveMode");
}

View File

@@ -37,9 +37,11 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
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.Session;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository.JdbcSession;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@@ -57,6 +59,7 @@ import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
@@ -227,6 +230,12 @@ class JdbcOperationsSessionRepositoryTests {
.withMessage("conversionService must not be null");
}
@Test
void setFlushModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setFlushMode(null))
.withMessage("flushMode must not be null");
}
@Test
void setSaveModeNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
@@ -254,6 +263,16 @@ class JdbcOperationsSessionRepositoryTests {
verifyZeroInteractions(this.jdbcOperations);
}
@Test
void createSessionImmediateFlushMode() {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isFalse();
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("INSERT"), isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void saveNewWithoutAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
@@ -773,6 +792,51 @@ class JdbcOperationsSessionRepositoryTests {
verifyZeroInteractions(this.jdbcOperations);
}
@Test
void flushModeImmediateSetAttribute() {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
String attrName = "someAttribute";
session.setAttribute(attrName, "someValue");
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("),
isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void flushModeImmediateRemoveAttribute() {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
MapSession cached = new MapSession();
cached.setAttribute("attribute1", "value1");
JdbcSession session = this.repository.new JdbcSession(cached, "primaryKey", false);
session.removeAttribute("attribute1");
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("DELETE FROM SPRING_SESSION_ATTRIBUTES WHERE"),
isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void flushModeSetMaxInactiveIntervalInSeconds() {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setMaxInactiveInterval(Duration.ofSeconds(1));
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION SET"), isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(this.jdbcOperations);
}
@Test
void flushModeSetLastAccessedTime() {
this.repository.setFlushMode(FlushMode.IMMEDIATE);
JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false);
session.setLastAccessedTime(Instant.now());
assertPropagationRequiresNew();
verify(this.jdbcOperations).update(startsWith("UPDATE SPRING_SESSION SET"), isA(PreparedStatementSetter.class));
verifyNoMoreInteractions(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.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
@@ -136,6 +137,20 @@ class JdbcHttpSessionConfigurationTests {
assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION);
}
@Test
void customFlushModeAnnotation() {
registerAndRefresh(DataSourceConfiguration.class, CustomFlushModeExpressionAnnotationConfiguration.class);
assertThat(this.context.getBean(JdbcHttpSessionConfiguration.class)).hasFieldOrPropertyWithValue("flushMode",
FlushMode.IMMEDIATE);
}
@Test
void customFlushModeSetter() {
registerAndRefresh(DataSourceConfiguration.class, CustomFlushModeExpressionSetterConfiguration.class);
assertThat(this.context.getBean(JdbcHttpSessionConfiguration.class)).hasFieldOrPropertyWithValue("flushMode",
FlushMode.IMMEDIATE);
}
@Test
void customSaveModeAnnotation() {
registerAndRefresh(DataSourceConfiguration.class, CustomSaveModeExpressionAnnotationConfiguration.class);
@@ -315,6 +330,20 @@ class JdbcHttpSessionConfigurationTests {
}
@EnableJdbcHttpSession(flushMode = FlushMode.IMMEDIATE)
static class CustomFlushModeExpressionAnnotationConfiguration {
}
@Configuration
static class CustomFlushModeExpressionSetterConfiguration extends JdbcHttpSessionConfiguration {
CustomFlushModeExpressionSetterConfiguration() {
setFlushMode(FlushMode.IMMEDIATE);
}
}
@EnableJdbcHttpSession(saveMode = SaveMode.ALWAYS)
static class CustomSaveModeExpressionAnnotationConfiguration {