From a251ea8bc7248567ca59cad8a29ddfdcc909cc9f Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Thu, 5 May 2016 23:28:00 +0200 Subject: [PATCH] Add Spring Session JDBC database initializer See gh-5879 --- .../session/JdbcSessionConfiguration.java | 11 ++ .../JdbcSessionDatabaseInitializer.java | 84 ++++++++++++++ .../session/SessionProperties.java | 40 +++++++ .../SessionAutoConfigurationJdbcTests.java | 109 ++++++++++++++++++ .../SessionAutoConfigurationTests.java | 26 ----- .../resources/session/custom-schema-h2.sql | 20 ++++ .../appendix-application-properties.adoc | 2 + 7 files changed, 266 insertions(+), 26 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java create mode 100644 spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java index b4fae61db43..d35d8d2e9f5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java @@ -21,8 +21,10 @@ import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; import org.springframework.session.SessionRepository; import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration; @@ -31,6 +33,7 @@ import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessi * * @author EddĂș MelĂ©ndez * @author Stephane Nicoll + * @author Vedran Pavic */ @Configuration @ConditionalOnMissingBean(SessionRepository.class) @@ -38,6 +41,14 @@ import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessi @Conditional(SessionCondition.class) class JdbcSessionConfiguration { + @Bean + @ConditionalOnMissingBean + public JdbcSessionDatabaseInitializer jdbcSessionDatabaseInitializer( + SessionProperties properties, DataSource dataSource, + ResourceLoader resourceLoader) { + return new JdbcSessionDatabaseInitializer(properties, dataSource, resourceLoader); + } + @Configuration public static class SpringBootJdbcHttpSessionConfiguration extends JdbcHttpSessionConfiguration { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java new file mode 100644 index 00000000000..cb4c93310e3 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.boot.autoconfigure.session; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; + +import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; +import org.springframework.util.Assert; + +/** + * Initializer for Spring Session schema. + * + * @author Vedran Pavic + * @since 1.4.0 + */ +public class JdbcSessionDatabaseInitializer { + + private SessionProperties properties; + + private DataSource dataSource; + + private ResourceLoader resourceLoader; + + public JdbcSessionDatabaseInitializer(SessionProperties properties, + DataSource dataSource, ResourceLoader resourceLoader) { + Assert.notNull(properties, "SessionProperties must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.properties = properties; + this.dataSource = dataSource; + this.resourceLoader = resourceLoader; + } + + @PostConstruct + protected void initialize() { + if (!this.properties.getJdbc().getInitializer().isEnabled()) { + return; + } + String platform = getDatabaseType(); + if ("hsql".equals(platform)) { + platform = "hsqldb"; + } + if ("postgres".equals(platform)) { + platform = "postgresql"; + } + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + String schemaLocation = this.properties.getJdbc().getSchema(); + schemaLocation = schemaLocation.replace("@@platform@@", platform); + populator.addScript(this.resourceLoader.getResource(schemaLocation)); + populator.setContinueOnError(true); + DatabasePopulatorUtils.execute(populator, this.dataSource); + } + + private String getDatabaseType() { + try { + String databaseProductName = JdbcUtils.extractDatabaseMetaData( + this.dataSource, "getDatabaseProductName").toString(); + return JdbcUtils.commonDatabaseName(databaseProductName).toLowerCase(); + } + catch (MetaDataAccessException ex) { + throw new IllegalStateException("Unable to detect database type", ex); + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java index 180402d189d..5fa4c607192 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java @@ -26,6 +26,7 @@ import org.springframework.session.data.redis.RedisFlushMode; * * @author Tommy Ludwig * @author Stephane Nicoll + * @author Vedran Pavic * @since 1.4.0 */ @ConfigurationProperties("spring.session") @@ -103,11 +104,29 @@ public class SessionProperties { public static class Jdbc { + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" + + "session/jdbc/schema-@@platform@@.sql"; + + /** + * Path to the SQL file to use to initialize the database schema. + */ + private String schema = DEFAULT_SCHEMA_LOCATION; + /** * Name of database table used to store sessions. */ private String tableName = "SPRING_SESSION"; + private final Initializer initializer = new Initializer(); + + public String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + public String getTableName() { return this.tableName; } @@ -116,6 +135,27 @@ public class SessionProperties { this.tableName = tableName; } + public Initializer getInitializer() { + return initializer; + } + + public static class Initializer { + + /** + * Create the required session tables on startup if necessary. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } public static class Mongo { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java new file mode 100644 index 00000000000..23b083a657c --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.boot.autoconfigure.session; + +import java.util.Arrays; + +import javax.persistence.EntityManagerFactory; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.session.jdbc.JdbcOperationsSessionRepository; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * JDBC specific tests for {@link SessionAutoConfiguration}. + * + * @author Vedran Pavic + */ +public class SessionAutoConfigurationJdbcTests extends AbstractSessionAutoConfigurationTests { + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Test + public void defaultConfig() { + load(Arrays.asList(EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class), + "spring.session.store-type=jdbc"); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("SPRING_SESSION"); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from SPRING_SESSION")).isEmpty(); + } + + @Test + public void usingJpa() { + load(Arrays.>asList(EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class), + "spring.session.store-type=jdbc"); + PlatformTransactionManager transactionManager = this.context + .getBean(PlatformTransactionManager.class); + assertThat(transactionManager.toString().contains("JpaTransactionManager")) + .isTrue(); + assertThat(this.context.getBean(EntityManagerFactory.class)).isNotNull(); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("SPRING_SESSION"); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from SPRING_SESSION")).isEmpty(); + } + + @Test + public void disableDatabaseInitializer() { + load(Arrays.asList(EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class), + "spring.session.store-type=jdbc", + "spring.session.jdbc.initializer.enabled=false"); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("SPRING_SESSION"); + this.expected.expect(BadSqlGrammarException.class); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from SPRING_SESSION")).isEmpty(); + } + + @Test + public void customTableName() { + load(Arrays.asList(EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class), + "spring.session.store-type=jdbc", + "spring.session.jdbc.table-name=FOO_BAR", + "spring.session.jdbc.schema=classpath:session/custom-schema-h2.sql"); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("FOO_BAR"); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from FOO_BAR")).isEmpty(); + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index 99877075e0d..4519e002e39 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -29,8 +29,6 @@ import org.junit.rules.ExpectedException; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -39,7 +37,6 @@ import org.springframework.session.ExpiringSession; import org.springframework.session.MapSessionRepository; import org.springframework.session.SessionRepository; import org.springframework.session.data.mongo.MongoOperationsSessionRepository; -import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -106,29 +103,6 @@ public class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurat assertThat(getSessionTimeout(repository)).isNull(); } - @Test - public void jdbcSessionStore() { - load(Arrays.asList(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class), - "spring.session.store-type=jdbc"); - JdbcOperationsSessionRepository repository = validateSessionRepository( - JdbcOperationsSessionRepository.class); - assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) - .isEqualTo("SPRING_SESSION"); - } - - @Test - public void jdbcSessionStoreCustomTableName() { - load(Arrays.asList(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class), - "spring.session.store-type=jdbc", - "spring.session.jdbc.table-name=FOO_BAR"); - JdbcOperationsSessionRepository repository = validateSessionRepository( - JdbcOperationsSessionRepository.class); - assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) - .isEqualTo("FOO_BAR"); - } - @Test public void hazelcastSessionStore() { load(Collections.>singletonList(HazelcastConfiguration.class), diff --git a/spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql b/spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql new file mode 100644 index 00000000000..27fd86ef43d --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql @@ -0,0 +1,20 @@ +CREATE TABLE FOO_BAR ( + SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, + LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, + PRINCIPAL_NAME VARCHAR(100), + CONSTRAINT FOO_BAR_PK PRIMARY KEY (SESSION_ID) +); + +CREATE INDEX FOO_BAR_IX1 ON FOO_BAR (LAST_ACCESS_TIME); + +CREATE TABLE FOO_BAR_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES LONGVARBINARY, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES FOO_BAR(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX FOO_BAR_ATTRIBUTES_IX1 ON FOO_BAR_ATTRIBUTES (SESSION_ID); diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 8eb852c0b09..74c3045353d 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -367,6 +367,8 @@ content into your application; rather pick only the properties that you need. # SPRING SESSION ({sc-spring-boot-autoconfigure}/session/SessionProperties.{sc-ext}[SessionProperties]) spring.session.hazelcast.map-name=spring:session:sessions # Name of the map used to store sessions. + spring.session.jdbc.initializer.enabled=true # Create the required session tables on startup if necessary. + spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema. spring.session.jdbc.table-name=SPRING_SESSION # Name of database table used to store sessions. spring.session.mongo.collection-name=sessions # Collection name used to store sessions. spring.session.redis.flush-mode= # Flush mode for the Redis sessions.