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
This commit is contained in:
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…
Reference in New Issue