diff --git a/docs/src/docs/asciidoc/guides/httpsession-jdbc-xml.adoc b/docs/src/docs/asciidoc/guides/httpsession-jdbc-xml.adoc index a795a35..c0a7d89 100644 --- a/docs/src/docs/asciidoc/guides/httpsession-jdbc-xml.adoc +++ b/docs/src/docs/asciidoc/guides/httpsession-jdbc-xml.adoc @@ -85,6 +85,7 @@ The filter is what is in charge of replacing the `HttpSession` implementation to In this instance Spring Session is backed by a relational database. <2> We create a `dataSource` that connects Spring Session to an embedded instance of H2 database. We configure the H2 database to create database tables using the SQL script which is included in Spring Session. +<3> We create a `transactionManager` that manages transactions for previously configured `dataSource`. == XML Servlet Container Initialization diff --git a/docs/src/docs/asciidoc/guides/httpsession-jdbc.adoc b/docs/src/docs/asciidoc/guides/httpsession-jdbc.adoc index 6437b65..178d659 100644 --- a/docs/src/docs/asciidoc/guides/httpsession-jdbc.adoc +++ b/docs/src/docs/asciidoc/guides/httpsession-jdbc.adoc @@ -83,6 +83,7 @@ The filter is what is in charge of replacing the `HttpSession` implementation to In this instance Spring Session is backed by a relational database. <2> We create a `dataSource` that connects Spring Session to an embedded instance of H2 database. We configure the H2 database to create database tables using the SQL script which is included in Spring Session. +<3> We create a `transactionManager` that manages transactions for previously configured `dataSource`. == Java Servlet Container Initialization diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 6637617..960e421 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -1012,7 +1012,7 @@ A typical example of how to create a new instance can be seen below: include::{indexdoc-tests}[tags=new-jdbcoperationssessionrepository] ---- -For additional information on how to create and configure a `JdbcTemplate`, refer to the Spring Framework Reference Documentation. +For additional information on how to create and configure `JdbcTemplate` and `PlatformTransactionManager`, refer to the Spring Framework Reference Documentation. [[api-jdbcoperationssessionrepository-config]] ==== EnableJdbcHttpSession @@ -1056,6 +1056,11 @@ And with MySQL database: include::{session-main-resources-dir}org/springframework/session/jdbc/schema-mysql.sql[] ---- +==== Transaction management + +All JDBC operations in `JdbcOperationsSessionRepository` are executed in a transactional manner. +Transactions are executed with propagation set to `REQUIRES_NEW` in order to avoid unexpected behavior due to interference with existing transactions (for example, executing `save` operation in a thread that already participates in a read-only transaction). + [[community]] == Spring Session Community diff --git a/docs/src/test/java/docs/IndexDocTests.java b/docs/src/test/java/docs/IndexDocTests.java index 09e376a..de21388 100644 --- a/docs/src/test/java/docs/IndexDocTests.java +++ b/docs/src/test/java/docs/IndexDocTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.mock.web.MockServletContext; import org.springframework.session.ExpiringSession; import org.springframework.session.MapSessionRepository; @@ -28,6 +29,7 @@ import org.springframework.session.SessionRepository; import org.springframework.session.data.redis.RedisOperationsSessionRepository; import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import org.springframework.session.web.http.SessionRepositoryFilter; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; @@ -124,8 +126,12 @@ public class IndexDocTests { // ... configure JdbcTemplate ... + PlatformTransactionManager transactionManager = new DataSourceTransactionManager(); + + // ... configure transactionManager ... + SessionRepository repository = - new JdbcOperationsSessionRepository(jdbcTemplate); + new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager); // end::new-jdbcoperationssessionrepository[] } diff --git a/spring-session/src/integration-test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryITests.java b/spring-session/src/integration-test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryITests.java index 0758869..d09e427 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryITests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryITests.java @@ -46,6 +46,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; @@ -114,6 +115,15 @@ public class JdbcOperationsSessionRepositoryITests { assertThat(this.repository.getSession(toSave.getId())).isNull(); } + @Test + @Transactional(readOnly = true) + public void savesInReadOnlyTransaction() { + JdbcOperationsSessionRepository.JdbcSession toSave = this.repository + .createSession(); + + this.repository.save(toSave); + } + @Test public void putAllOnSingleAttrDoesNotRemoveOld() { JdbcOperationsSessionRepository.JdbcSession toSave = this.repository diff --git a/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java index a515cdb..720343b 100644 --- a/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java @@ -50,6 +50,13 @@ import org.springframework.session.ExpiringSession; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; import org.springframework.session.Session; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionOperations; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -63,7 +70,14 @@ import org.springframework.util.StringUtils; *
  * JdbcTemplate jdbcTemplate = new JdbcTemplate();
  *
- * JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository(jdbcTemplate);
+ * // ... configure jdbcTemplate ...
+ *
+ * PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
+ *
+ * // ... configure transactionManager ...
+ *
+ * JdbcOperationsSessionRepository sessionRepository =
+ *         new JdbcOperationsSessionRepository(jdbcTemplate, transactionManager);
  * 
* * For additional information on how to create and configure a JdbcTemplate, refer to the @@ -121,6 +135,8 @@ public class JdbcOperationsSessionRepository implements private final JdbcOperations jdbcOperations; + private final TransactionOperations transactionOperations; + private final RowMapper mapper = new ExpiringSessionMapper(); /** @@ -140,22 +156,26 @@ public class JdbcOperationsSessionRepository implements /** * Create a new {@link JdbcOperationsSessionRepository} instance which uses the - * default ${JdbcOperations} to manage sessions. + * default {@link JdbcOperations} to manage sessions. * @param dataSource the {@link DataSource} to use + * @param transactionManager the {@link PlatformTransactionManager} to use */ - public JdbcOperationsSessionRepository(DataSource dataSource) { - this(createDefaultTemplate(dataSource)); + public JdbcOperationsSessionRepository(DataSource dataSource, + PlatformTransactionManager transactionManager) { + this(createDefaultJdbcTemplate(dataSource), transactionManager); } /** * Create a new {@link JdbcOperationsSessionRepository} instance which uses the - * provided ${JdbcOperations} to manage sessions. + * provided {@link JdbcOperations} to manage sessions. * @param jdbcOperations the {@link JdbcOperations} to use + * @param transactionManager the {@link PlatformTransactionManager} to use */ - public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations) { + public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations, + PlatformTransactionManager transactionManager) { Assert.notNull(jdbcOperations, "JdbcOperations must not be null"); this.jdbcOperations = jdbcOperations; - + this.transactionOperations = createTransactionTemplate(transactionManager); this.conversionService = createDefaultConversionService(); } @@ -185,7 +205,6 @@ public class JdbcOperationsSessionRepository implements /** * Sets the {@link ConversionService} to use. - * * @param conversionService the converter to set */ public void setConversionService(ConversionService conversionService) { @@ -203,49 +222,65 @@ public class JdbcOperationsSessionRepository implements public void save(final JdbcSession session) { if (session.isNew()) { - this.jdbcOperations.update(getQuery(CREATE_SESSION_QUERY), - new PreparedStatementSetter() { + this.transactionOperations.execute(new TransactionCallbackWithoutResult() { - public void setValues(PreparedStatement ps) throws SQLException { - ps.setString(1, session.getId()); - ps.setLong(2, session.getLastAccessedTime()); - ps.setString(3, session.getPrincipalName()); - JdbcOperationsSessionRepository.this.lobHandler - .getLobCreator() - .setBlobAsBytes(ps, 4, serialize(session.delegate)); - } + protected void doInTransactionWithoutResult(TransactionStatus status) { + JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(CREATE_SESSION_QUERY), + new PreparedStatementSetter() { - }); + public void setValues(PreparedStatement ps) throws SQLException { + ps.setString(1, session.getId()); + ps.setLong(2, session.getLastAccessedTime()); + ps.setString(3, session.getPrincipalName()); + serialize(ps, 4, session.delegate); + } + + }); + } + + }); } else { if (session.isAttributesChanged()) { - this.jdbcOperations.update(getQuery(UPDATE_SESSION_QUERY), - new PreparedStatementSetter() { + this.transactionOperations.execute(new TransactionCallbackWithoutResult() { - public void setValues(PreparedStatement ps) - throws SQLException { - ps.setLong(1, session.getLastAccessedTime()); - ps.setString(2, session.getPrincipalName()); - JdbcOperationsSessionRepository.this.lobHandler - .getLobCreator().setBlobAsBytes(ps, 3, - serialize(session.delegate)); - ps.setString(4, session.getId()); - } + protected void doInTransactionWithoutResult(TransactionStatus status) { + JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(UPDATE_SESSION_QUERY), + new PreparedStatementSetter() { - }); + public void setValues(PreparedStatement ps) + throws SQLException { + ps.setLong(1, session.getLastAccessedTime()); + ps.setString(2, session.getPrincipalName()); + serialize(ps, 3, session.delegate); + ps.setString(4, session.getId()); + } + + }); + } + + }); } else if (session.isLastAccessTimeChanged()) { - this.jdbcOperations.update( - getQuery(UPDATE_SESSION_LAST_ACCESS_TIME_QUERY), - new PreparedStatementSetter() { + this.transactionOperations.execute(new TransactionCallbackWithoutResult() { - public void setValues(PreparedStatement ps) - throws SQLException { - ps.setLong(1, session.getLastAccessedTime()); - ps.setString(2, session.getId()); - } + protected void doInTransactionWithoutResult(TransactionStatus status) { + JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(UPDATE_SESSION_LAST_ACCESS_TIME_QUERY), + new PreparedStatementSetter() { - }); + public void setValues(PreparedStatement ps) + throws SQLException { + ps.setLong(1, session.getLastAccessedTime()); + ps.setString(2, session.getId()); + } + + }); + } + + }); } else { return; @@ -254,14 +289,22 @@ public class JdbcOperationsSessionRepository implements session.clearChangeFlags(); } - public JdbcSession getSession(String id) { - ExpiringSession session = null; - try { - session = this.jdbcOperations.queryForObject(getQuery(GET_SESSION_QUERY), - new Object[] { id }, this.mapper); - } - catch (EmptyResultDataAccessException ignored) { - } + public JdbcSession getSession(final String id) { + ExpiringSession session = this.transactionOperations.execute(new TransactionCallback() { + + public ExpiringSession doInTransaction(TransactionStatus status) { + try { + return JdbcOperationsSessionRepository.this.jdbcOperations.queryForObject( + getQuery(GET_SESSION_QUERY), + new Object[] { id }, + JdbcOperationsSessionRepository.this.mapper); + } + catch (EmptyResultDataAccessException ignored) { + return null; + } + } + + }); if (session != null) { if (session.isExpired()) { @@ -274,19 +317,33 @@ public class JdbcOperationsSessionRepository implements return null; } - public void delete(String id) { - this.jdbcOperations.update(getQuery(DELETE_SESSION_QUERY), id); + public void delete(final String id) { + this.transactionOperations.execute(new TransactionCallbackWithoutResult() { + + protected void doInTransactionWithoutResult(TransactionStatus status) { + JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(DELETE_SESSION_QUERY), id); + } + + }); } public Map findByIndexNameAndIndexValue(String indexName, - String indexValue) { + final String indexValue) { if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) { return Collections.emptyMap(); } - List sessions = this.jdbcOperations.query( - getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY), - new Object[] { indexValue }, this.mapper); + List sessions = this.transactionOperations.execute(new TransactionCallback>() { + + public List doInTransaction(TransactionStatus status) { + return JdbcOperationsSessionRepository.this.jdbcOperations.query( + getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY), + new Object[] { indexValue }, + JdbcOperationsSessionRepository.this.mapper); + } + + }); Map sessionMap = new HashMap( sessions.size()); @@ -305,36 +362,42 @@ public class JdbcOperationsSessionRepository implements ? this.defaultMaxInactiveInterval : MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; - long sessionsValidFromTime = now - (maxInactiveIntervalSeconds * 1000); + final long sessionsValidFromTime = now - (maxInactiveIntervalSeconds * 1000); if (logger.isDebugEnabled()) { logger.debug( "Cleaning up sessions older than " + new Date(sessionsValidFromTime)); } - int deletedCount = this.jdbcOperations.update( - getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY), - sessionsValidFromTime); + int deletedCount = this.transactionOperations.execute(new TransactionCallback() { + + public Integer doInTransaction(TransactionStatus transactionStatus) { + return JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY), + sessionsValidFromTime); + } + + }); if (logger.isDebugEnabled()) { logger.debug("Cleaned up " + deletedCount + " expired sessions"); } } - private static JdbcTemplate createDefaultTemplate(DataSource dataSource) { + private static JdbcTemplate createDefaultJdbcTemplate(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.afterPropertiesSet(); return jdbcTemplate; } - protected String getQuery(String base) { - return StringUtils.replace(base, "%TABLE_NAME%", this.tableName); - } - - private byte[] serialize(ExpiringSession session) { - return (byte[]) this.conversionService.convert(session, - TypeDescriptor.valueOf(ExpiringSession.class), - TypeDescriptor.valueOf(byte[].class)); + private static TransactionTemplate createTransactionTemplate( + PlatformTransactionManager transactionManager) { + TransactionTemplate transactionTemplate = new TransactionTemplate( + transactionManager); + transactionTemplate.setPropagationBehavior( + TransactionDefinition.PROPAGATION_REQUIRES_NEW); + transactionTemplate.afterPropertiesSet(); + return transactionTemplate; } private static GenericConversionService createDefaultConversionService() { @@ -346,6 +409,26 @@ public class JdbcOperationsSessionRepository implements return converter; } + protected String getQuery(String base) { + return StringUtils.replace(base, "%TABLE_NAME%", this.tableName); + } + + private void serialize(PreparedStatement ps, int paramIndex, ExpiringSession session) + throws SQLException { + this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex, + (byte[]) this.conversionService.convert(session, + TypeDescriptor.valueOf(ExpiringSession.class), + TypeDescriptor.valueOf(byte[].class))); + } + + private ExpiringSession deserialize(ResultSet rs, String columnName) + throws SQLException { + return (ExpiringSession) this.conversionService.convert( + this.lobHandler.getBlobAsBytes(rs, columnName), + TypeDescriptor.valueOf(byte[].class), + TypeDescriptor.valueOf(ExpiringSession.class)); + } + /** * The {@link ExpiringSession} to use for {@link JdbcOperationsSessionRepository}. * @@ -473,12 +556,7 @@ public class JdbcOperationsSessionRepository implements private class ExpiringSessionMapper implements RowMapper { public ExpiringSession mapRow(ResultSet rs, int rowNum) throws SQLException { - ExpiringSession session = (ExpiringSession) JdbcOperationsSessionRepository - .this.conversionService.convert( - JdbcOperationsSessionRepository.this.lobHandler - .getBlobAsBytes(rs, "SESSION_BYTES"), - TypeDescriptor.valueOf(byte[].class), - TypeDescriptor.valueOf(ExpiringSession.class)); + ExpiringSession session = deserialize(rs, "SESSION_BYTES"); session.setLastAccessedTime(rs.getLong("LAST_ACCESS_TIME")); return session; } diff --git a/spring-session/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java b/spring-session/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java index dd49a69..278ef9f 100644 --- a/spring-session/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java +++ b/spring-session/src/main/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.jdbc.support.lob.LobHandler; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; import org.springframework.session.jdbc.JdbcOperationsSessionRepository; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.StringUtils; /** @@ -71,9 +72,10 @@ public class JdbcHttpSessionConfiguration extends SpringHttpSessionConfiguration @Bean public JdbcOperationsSessionRepository sessionRepository( - @Qualifier("springSessionJdbcOperations") JdbcOperations jdbcOperations) { - JdbcOperationsSessionRepository sessionRepository = new JdbcOperationsSessionRepository( - jdbcOperations); + @Qualifier("springSessionJdbcOperations") JdbcOperations jdbcOperations, + PlatformTransactionManager transactionManager) { + JdbcOperationsSessionRepository sessionRepository = + new JdbcOperationsSessionRepository(jdbcOperations, transactionManager); String tableName = getTableName(); if (StringUtils.hasText(tableName)) { sessionRepository.setTableName(tableName); diff --git a/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java b/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java index b2e5da1..077fdb8 100644 --- a/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java +++ b/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java @@ -26,6 +26,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -38,6 +39,8 @@ import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.AdditionalMatchers.and; @@ -48,6 +51,7 @@ import static org.mockito.Matchers.contains; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isA; import static org.mockito.Matchers.startsWith; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -72,17 +76,21 @@ public class JdbcOperationsSessionRepositoryTests { @Mock private JdbcOperations jdbcOperations; + @Mock + private PlatformTransactionManager transactionManager; + private JdbcOperationsSessionRepository repository; @Before public void setUp() { - this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations); + this.repository = new JdbcOperationsSessionRepository( + this.jdbcOperations, this.transactionManager); } @Test public void constructorDataSource() { JdbcOperationsSessionRepository repository = new JdbcOperationsSessionRepository( - this.dataSource); + this.dataSource, this.transactionManager); assertThat(ReflectionTestUtils.getField(repository, "jdbcOperations")) .isNotNull(); @@ -93,7 +101,7 @@ public class JdbcOperationsSessionRepositoryTests { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Property 'dataSource' is required"); - new JdbcOperationsSessionRepository((DataSource) null); + new JdbcOperationsSessionRepository((DataSource) null, this.transactionManager); } @Test @@ -101,7 +109,15 @@ public class JdbcOperationsSessionRepositoryTests { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("JdbcOperations must not be null"); - new JdbcOperationsSessionRepository((JdbcOperations) null); + new JdbcOperationsSessionRepository((JdbcOperations) null, this.transactionManager); + } + + @Test + public void constructorNullTransactionManager() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Property 'transactionManager' is required"); + + new JdbcOperationsSessionRepository(this.jdbcOperations, null); } @Test @@ -168,6 +184,7 @@ public class JdbcOperationsSessionRepositoryTests { this.repository.save(session); assertThat(session.isNew()).isFalse(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"), isA(PreparedStatementSetter.class)); } @@ -181,6 +198,7 @@ public class JdbcOperationsSessionRepositoryTests { this.repository.save(session); assertThat(session.isNew()).isFalse(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update( and(startsWith("UPDATE"), contains("SESSION_BYTES")), isA(PreparedStatementSetter.class)); @@ -195,6 +213,7 @@ public class JdbcOperationsSessionRepositoryTests { this.repository.save(session); assertThat(session.isNew()).isFalse(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update( and(startsWith("UPDATE"), not(contains("SESSION_BYTES"))), isA(PreparedStatementSetter.class)); @@ -219,6 +238,7 @@ public class JdbcOperationsSessionRepositoryTests { .getSession(sessionId); assertThat(session).isNull(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"), eq(new Object[] { sessionId }), isA(RowMapper.class)); } @@ -236,6 +256,7 @@ public class JdbcOperationsSessionRepositoryTests { .getSession(expired.getId()); assertThat(session).isNull(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"), eq(new Object[] { expired.getId() }), isA(RowMapper.class)); verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), @@ -256,6 +277,7 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(session.getId()).isEqualTo(saved.getId()); assertThat(session.isNew()).isFalse(); assertThat(session.getAttribute("savedName")).isEqualTo("savedValue"); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"), eq(new Object[] { saved.getId() }), isA(RowMapper.class)); } @@ -266,6 +288,7 @@ public class JdbcOperationsSessionRepositoryTests { this.repository.delete(sessionId); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(sessionId)); } @@ -290,6 +313,7 @@ public class JdbcOperationsSessionRepositoryTests { principal); assertThat(sessions).isEmpty(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).query(startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class)); } @@ -315,6 +339,7 @@ public class JdbcOperationsSessionRepositoryTests { principal); assertThat(sessions).hasSize(2); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).query(startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class)); } @@ -323,7 +348,16 @@ public class JdbcOperationsSessionRepositoryTests { public void cleanupExpiredSessions() { this.repository.cleanUpExpiredSessions(); + assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong()); } + private void assertPropagationRequiresNew() { + ArgumentCaptor argument = + ArgumentCaptor.forClass(TransactionDefinition.class); + verify(this.transactionManager, atLeastOnce()).getTransaction(argument.capture()); + assertThat(argument.getValue().getPropagationBehavior()) + .isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + } + } diff --git a/spring-session/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java b/spring-session/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java index 628653b..b6eb449 100644 --- a/spring-session/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java +++ b/spring-session/src/test/java/org/springframework/session/jdbc/config/annotation/web/http/JdbcHttpSessionConfigurationTests.java @@ -31,6 +31,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.jdbc.support.lob.LobHandler; import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.PlatformTransactionManager; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -162,6 +163,11 @@ public class JdbcHttpSessionConfigurationTests { return mock(DataSource.class); } + @Bean + public PlatformTransactionManager transactionManager() { + return mock(PlatformTransactionManager.class); + } + } @Configuration