Merge pull request #5879 from vpavic:session-jdbc-initializer

* pr/5879:
  Polish "Add Spring Session JDBC database initializer"
  Add Spring Session JDBC database initializer
  Allow Tomcat context root redirect to be configured via the environment
pull/6241/merge
Stephane Nicoll 9 years ago
commit 31a5d7f876

@ -20,9 +20,13 @@ import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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.jdbc.core.JdbcTemplate;
import org.springframework.session.SessionRepository;
import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration;
@ -31,13 +35,23 @@ import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessi
*
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Vedran Pavic
*/
@Configuration
@ConditionalOnClass(JdbcTemplate.class)
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(DataSource.class)
@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 {

@ -0,0 +1,83 @@
/*
* 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()) {
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);
}
}
}

@ -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 {

@ -646,6 +646,12 @@ public class ServerProperties
*/
private int maxHttpHeaderSize = 0; // bytes
/**
* Whether requests to the context root should be redirected by appending a / to
* the path.
*/
private Boolean redirectContextRoot;
/**
* Character encoding to use to decode the URI.
*/
@ -742,6 +748,14 @@ public class ServerProperties
this.portHeader = portHeader;
}
public Boolean getRedirectContextRoot() {
return this.redirectContextRoot;
}
public void setRedirectContextRoot(Boolean redirectContextRoot) {
this.redirectContextRoot = redirectContextRoot;
}
public String getRemoteIpHeader() {
return this.remoteIpHeader;
}
@ -789,6 +803,9 @@ public class ServerProperties
customizeConnectionTimeout(factory,
serverProperties.getConnectionTimeout());
}
if (this.redirectContextRoot != null) {
customizeRedirectContextRoot(factory, this.redirectContextRoot);
}
}
private void customizeConnectionTimeout(
@ -911,6 +928,19 @@ public class ServerProperties
factory.addContextValves(valve);
}
private void customizeRedirectContextRoot(
TomcatEmbeddedServletContainerFactory factory,
final boolean redirectContextRoot) {
factory.addContextCustomizers(new TomcatContextCustomizer() {
@Override
public void customize(Context context) {
context.setMapperContextRootRedirectEnabled(redirectContextRoot);
}
});
}
public static class Accesslog {
/**

@ -0,0 +1,88 @@
/*
* 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 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.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
/**
* JDBC specific tests for {@link SessionAutoConfiguration}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
*/
public class SessionAutoConfigurationJdbcTests extends AbstractSessionAutoConfigurationTests {
@Rule
public ExpectedException thrown = 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 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.thrown.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();
}
}

@ -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.<Class<?>>singletonList(HazelcastConfiguration.class),

@ -29,6 +29,7 @@ import javax.servlet.ServletException;
import javax.servlet.SessionCookieConfig;
import javax.servlet.SessionTrackingMode;
import org.apache.catalina.Context;
import org.apache.catalina.Valve;
import org.apache.catalina.valves.RemoteIpValve;
import org.junit.Before;
@ -41,6 +42,7 @@ import org.springframework.beans.MutablePropertyValues;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
import org.springframework.boot.web.servlet.ServletContextInitializer;
@ -150,6 +152,30 @@ public class ServerPropertiesTests {
.isEqualTo("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
}
@Test
public void redirectContextRootIsNotConfiguredByDefault() throws Exception {
bindProperties(new HashMap<String, String>());
ServerProperties.Tomcat tomcat = this.properties.getTomcat();
assertThat(tomcat.getRedirectContextRoot()).isNull();
}
@Test
public void redirectContextRootCanBeConfigured() throws Exception {
Map<String, String> map = new HashMap<String, String>();
map.put("server.tomcat.redirect-context-root", "false");
bindProperties(map);
ServerProperties.Tomcat tomcat = this.properties.getTomcat();
assertThat(tomcat.getRedirectContextRoot()).isEqualTo(false);
TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory();
this.properties.customize(container);
Context context = mock(Context.class);
for (TomcatContextCustomizer customizer : container
.getTomcatContextCustomizers()) {
customizer.customize(context);
}
verify(context).setMapperContextRootRedirectEnabled(false);
}
@Test
public void testTrailingSlashOfContextPathIsRemoved() {
new RelaxedDataBinder(this.properties, "server").bind(new MutablePropertyValues(

@ -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);

@ -214,6 +214,7 @@ content into your application; rather pick only the properties that you need.
server.tomcat.port-header=X-Forwarded-Port # Name of the HTTP header used to override the original port value.
server.tomcat.protocol-header= # Header that holds the incoming protocol, usually named "X-Forwarded-Proto".
server.tomcat.protocol-header-https-value=https # Value of the protocol header that indicates that the incoming request uses SSL.
server.tomcat.redirect-context-root= # Whether requests to the context root should be redirected by appending a / to the path.
server.tomcat.remote-ip-header= # Name of the http header from which the remote ip is extracted. For instance `X-FORWARDED-FOR`
server.tomcat.uri-encoding=UTF-8 # Character encoding to use to decode the URI.
server.undertow.accesslog.dir= # Undertow access log directory.
@ -366,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.

@ -184,7 +184,6 @@ public class TomcatEmbeddedServletContainerFactory
: ClassUtils.getDefaultClassLoader());
try {
context.setUseRelativeRedirects(false);
context.setMapperContextRootRedirectEnabled(true);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.0.30. Continue

Loading…
Cancel
Save