diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryException.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryException.java new file mode 100644 index 00000000000..aa662b1848e --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2017 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.Collections; +import java.util.List; + +import org.springframework.session.SessionRepository; +import org.springframework.util.ObjectUtils; + +/** + * Exception thrown when multiple {@link SessionRepository} implementations are + * available with no way to know which implementation should be used. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class NonUniqueSessionRepositoryException extends RuntimeException { + + private final List> availableCandidates; + + public NonUniqueSessionRepositoryException( + List> availableCandidates) { + super("Multiple session repository candidates are available, set the " + + "'spring.session.store-type' property accordingly"); + this.availableCandidates = (!ObjectUtils.isEmpty(availableCandidates) + ? availableCandidates : Collections.emptyList()); + } + + public List> getAvailableCandidates() { + return this.availableCandidates; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java new file mode 100644 index 00000000000..ab47d11de70 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2017 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 org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.session.SessionRepository; + +/** + * A {@link AbstractFailureAnalyzer} for {@link NonUniqueSessionRepositoryException}. + * + * @author Stephane Nicoll + */ +class NonUniqueSessionRepositoryFailureAnalyzer + extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + NonUniqueSessionRepositoryException cause) { + StringBuilder message = new StringBuilder(); + message.append(String.format("Multiple Spring Session store implementations are " + + "available on the classpath:%n")); + for (Class candidate : cause.getAvailableCandidates()) { + message.append(String.format(" - %s%n", candidate.getName())); + } + StringBuilder action = new StringBuilder(); + action.append(String.format("Consider any of the following:%n")); + action.append(String.format(" - Define the `spring.session.store-type` " + + "property to the store you want to use%n")); + action.append(String.format(" - Review your classpath and remove the unwanted " + + "store implementation(s)%n")); + return new FailureAnalysis(message.toString(), action.toString(), cause); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java index 96711203aa1..8a8749e00ec 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.session; +import java.util.ArrayList; +import java.util.List; + import javax.annotation.PostConstruct; import org.springframework.beans.factory.ObjectProvider; @@ -32,6 +35,7 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration.SessionRepositoryConfiguration; import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration.SessionRepositoryValidator; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; @@ -61,7 +65,8 @@ public class SessionAutoConfiguration { @Configuration @ConditionalOnMissingBean(SessionRepository.class) - @Import(SessionConfigurationImportSelector.class) + @Import({ SessionRepositoryImplementationValidator.class, + SessionConfigurationImportSelector.class }) static class SessionRepositoryConfiguration { } @@ -83,6 +88,51 @@ public class SessionAutoConfiguration { } + /** + * Bean used to validate that only one supported implementation is available in the + * classpath when the store-type property is not set. + */ + static class SessionRepositoryImplementationValidator { + + private final ClassLoader classLoader; + + private final SessionProperties sessionProperties; + + SessionRepositoryImplementationValidator(ApplicationContext applicationContext, + SessionProperties sessionProperties) { + this.classLoader = applicationContext.getClassLoader(); + this.sessionProperties = sessionProperties; + } + + @PostConstruct + public void checkAvailableImplementations() { + List> candidates = new ArrayList<>(); + addCandidate(candidates, + "org.springframework.session.hazelcast.HazelcastSessionRepository"); + addCandidate(candidates, + "org.springframework.session.jdbc.JdbcOperationsSessionRepository"); + addCandidate(candidates, + "org.springframework.session.data.redis.RedisOperationsSessionRepository"); + StoreType storeType = this.sessionProperties.getStoreType(); + if (candidates.size() > 1 && storeType == null) { + throw new NonUniqueSessionRepositoryException(candidates); + } + } + + private void addCandidate( + List> candidates, String fqn) { + try { + Class candidate = (Class) this.classLoader.loadClass(fqn); + if (candidate != null) { + candidates.add(candidate); + } + } + catch (Throwable ex) { + // Ignore + } + } + } + /** * Bean used to validate that a {@link SessionRepository} exists and provide a * meaningful message if that's not the case. @@ -105,12 +155,11 @@ public class SessionAutoConfiguration { if (storeType != StoreType.NONE && this.sessionRepositoryProvider.getIfAvailable() == null) { if (storeType != null) { - throw new IllegalArgumentException("No session repository could be " - + "auto-configured, check your configuration (session store " - + "type is '" + storeType.name().toLowerCase() + "')"); + throw new SessionRepositoryUnavailableException("No session " + + "repository could be auto-configured, check your " + + "configuration (session store type is '" + + storeType.name().toLowerCase() + "')", storeType); } - throw new IllegalArgumentException("No Spring Session store is " - + "configured: set the 'spring.session.store-type' property"); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java index 5c142f104f4..d90c4ecc910 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java @@ -44,8 +44,8 @@ class SessionCondition extends SpringBootCondition { StoreType required = SessionStoreMappings .getType(((AnnotationMetadata) metadata).getClassName()); if (!environment.containsProperty("spring.session.store-type")) { - return ConditionOutcome.noMatch( - message.didNotFind("spring.session.store-type property").atAll()); + return ConditionOutcome.match(message.didNotFind("property", "properties") + .items(ConditionMessage.Style.QUOTE, "spring.session.store-type")); } try { Binder binder = Binder.get(environment); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryUnavailableException.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryUnavailableException.java new file mode 100644 index 00000000000..55535c59459 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryUnavailableException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2017 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 org.springframework.session.SessionRepository; + +/** + * Exception thrown when no {@link SessionRepository} is available. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class SessionRepositoryUnavailableException extends RuntimeException { + + private final StoreType storeType; + + public SessionRepositoryUnavailableException(String message, StoreType storeType) { + super(message); + this.storeType = storeType; + } + + public StoreType getStoreType() { + return this.storeType; + } + +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index ac220d1de2c..9324b916e3b 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -130,7 +130,8 @@ org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ -org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer +org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer # Template availability providers org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java new file mode 100644 index 00000000000..010b19f52a2 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2017 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.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; +import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter; +import org.springframework.session.SessionRepository; +import org.springframework.session.hazelcast.HazelcastSessionRepository; +import org.springframework.session.jdbc.JdbcOperationsSessionRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NonUniqueSessionRepositoryFailureAnalyzer}. + * + * @author Stephane Nicoll + */ +public class NonUniqueSessionRepositoryFailureAnalyzerTests { + + private final FailureAnalyzer analyzer = new NonUniqueSessionRepositoryFailureAnalyzer(); + + @Test + public void failureAnalysisWithMultipleCandidates() { + FailureAnalysis analysis = analyzeFailure(createFailure( + JdbcOperationsSessionRepository.class, HazelcastSessionRepository.class)); + assertThat(analysis).isNotNull(); + assertThat(analysis.getDescription()).contains( + JdbcOperationsSessionRepository.class.getName(), + HazelcastSessionRepository.class.getName()); + assertThat(analysis.getAction()).contains("spring.session.store-type"); + } + + private Exception createFailure(Class... candidates) { + return new NonUniqueSessionRepositoryException(Arrays.asList(candidates)); + } + + private FailureAnalysis analyzeFailure(Exception failure) { + FailureAnalysis analysis = this.analyzer.analyze(failure); + if (analysis != null) { + new LoggingFailureAnalysisReporter().report(analysis); + } + return analysis; + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java index 94449496e14..316e7d9cef5 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java @@ -22,11 +22,15 @@ import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.HideClassesClassLoader; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.session.data.redis.RedisOperationsSessionRepository; import org.springframework.session.hazelcast.HazelcastFlushMode; import org.springframework.session.hazelcast.HazelcastSessionRepository; +import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -48,13 +52,24 @@ public class SessionAutoConfigurationHazelcastTests @Test public void defaultConfig() { - this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast") - .run((context) -> { - validateSessionRepository(context, HazelcastSessionRepository.class); - HazelcastInstance hazelcastInstance = context - .getBean(HazelcastInstance.class); - verify(hazelcastInstance, times(1)).getMap("spring:session:sessions"); - }); + this.contextRunner + .withPropertyValues("spring.session.store-type=hazelcast") + .run(this::validateDefaultConfig); + } + + @Test + public void defaultConfigWithUniqueStoreImplementation() { + this.contextRunner.withClassLoader(new HideClassesClassLoader( + JdbcOperationsSessionRepository.class, + RedisOperationsSessionRepository.class)).run( + this::validateDefaultConfig); + } + + private void validateDefaultConfig(AssertableWebApplicationContext context) { + validateSessionRepository(context, HazelcastSessionRepository.class); + HazelcastInstance hazelcastInstance = context + .getBean(HazelcastInstance.class); + verify(hazelcastInstance, times(1)).getMap("spring:session:sessions"); } @Test @@ -80,7 +95,7 @@ public class SessionAutoConfigurationHazelcastTests context, HazelcastSessionRepository.class); assertThat(new DirectFieldAccessor(repository) .getPropertyValue("hazelcastFlushMode")) - .isEqualTo(HazelcastFlushMode.IMMEDIATE); + .isEqualTo(HazelcastFlushMode.IMMEDIATE); }); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000000..819ee574b6a --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2017 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 com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.IMap; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.jdbc.JdbcOperationsSessionRepository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link SessionAutoConfiguration}. + * + * @author Stephane Nicoll + */ +public class SessionAutoConfigurationIntegrationTests + extends AbstractSessionAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + SessionAutoConfiguration.class)) + .withPropertyValues("spring.datasource.generate-unique-name=true"); + + + @Test + public void severalCandidatesWithNoSessionStore() { + this.contextRunner.withUserConfiguration(HazelcastConfiguration.class).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure() + .hasCauseInstanceOf(NonUniqueSessionRepositoryException.class); + assertThat(context).getFailure().hasMessageContaining( + "Multiple session repository candidates are available"); + assertThat(context).getFailure().hasMessageContaining( + "set the 'spring.session.store-type' property accordingly"); + }); + } + + @Test + public void severalCandidatesWithWrongSessionStore() { + this.contextRunner.withUserConfiguration(HazelcastConfiguration.class) + .withPropertyValues("spring.session.store-type=redis").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure() + .hasCauseInstanceOf(SessionRepositoryUnavailableException.class); + assertThat(context).getFailure().hasMessageContaining( + "No session repository could be auto-configured"); + assertThat(context).getFailure().hasMessageContaining( + "session store type is 'redis'"); + }); + } + + @Test + public void severalCandidatesWithValidSessionStore() { + this.contextRunner.withUserConfiguration(HazelcastConfiguration.class) + .withPropertyValues("spring.session.store-type=jdbc") + .run((context) -> validateSessionRepository(context, + JdbcOperationsSessionRepository.class)); + } + + + @Configuration + static class HazelcastConfiguration { + + @Bean + @SuppressWarnings("unchecked") + public HazelcastInstance hazelcastInstance() { + IMap map = mock(IMap.class); + HazelcastInstance mock = mock(HazelcastInstance.class); + given(mock.getMap("spring:session:sessions")).willReturn(map); + given(mock.getMap("foo:bar:biz")).willReturn(map); + return mock; + } + + } + +} 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 index 4a4a1edaf27..a93f995f0bc 100644 --- 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 @@ -26,10 +26,14 @@ import org.springframework.boot.autoconfigure.DatabaseInitializationMode; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.test.context.HideClassesClassLoader; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.session.data.redis.RedisOperationsSessionRepository; +import org.springframework.session.hazelcast.HazelcastSessionRepository; import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import static org.assertj.core.api.Assertions.assertThat; @@ -55,19 +59,35 @@ public class SessionAutoConfigurationJdbcTests @Test public void defaultConfig() { this.contextRunner + .withPropertyValues("spring.session.store-type=jdbc") .withConfiguration( AutoConfigurations.of(JdbcTemplateAutoConfiguration.class)) - .withPropertyValues("spring.session.store-type=jdbc").run((context) -> { - JdbcOperationsSessionRepository repository = validateSessionRepository( - context, JdbcOperationsSessionRepository.class); - assertThat(new DirectFieldAccessor(repository) - .getPropertyValue("tableName")).isEqualTo("SPRING_SESSION"); - assertThat(context.getBean(JdbcSessionProperties.class) - .getInitializeSchema()) - .isEqualTo(DatabaseInitializationMode.EMBEDDED); - assertThat(context.getBean(JdbcOperations.class) - .queryForList("select * from SPRING_SESSION")).isEmpty(); - }); + .run(this::validateDefaultConfig); + } + + @Test + public void defaultConfigWithUniqueStoreImplementation() { + this.contextRunner + .withClassLoader(new HideClassesClassLoader( + HazelcastSessionRepository.class, + RedisOperationsSessionRepository.class) + ) + .withConfiguration( + AutoConfigurations.of(JdbcTemplateAutoConfiguration.class)) + .run(this::validateDefaultConfig); + } + + private void validateDefaultConfig(AssertableWebApplicationContext context) { + JdbcOperationsSessionRepository repository = validateSessionRepository( + context, JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository) + .getPropertyValue("tableName")).isEqualTo("SPRING_SESSION"); + assertThat(context.getBean(JdbcSessionProperties.class) + .getInitializeSchema()) + .isEqualTo(DatabaseInitializationMode.EMBEDDED); + assertThat(context.getBean(JdbcOperations.class) + .queryForList("select * from SPRING_SESSION")).isEmpty(); + } @Test diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java index 077751d494a..e19c2d764b7 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java @@ -22,12 +22,15 @@ import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.HideClassesClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.testsupport.rule.RedisTestServer; import org.springframework.session.data.redis.RedisFlushMode; import org.springframework.session.data.redis.RedisOperationsSessionRepository; +import org.springframework.session.hazelcast.HazelcastSessionRepository; +import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import static org.assertj.core.api.Assertions.assertThat; @@ -46,10 +49,21 @@ public class SessionAutoConfigurationRedisTests .withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class)); @Test - public void redisSessionStore() { + public void defaultConfig() { this.contextRunner - .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .withPropertyValues("spring.session.store-type=redis") + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) + .run(validateSpringSessionUsesRedis("spring:session:event:created:", + RedisFlushMode.ON_SAVE)); + } + + @Test + public void defaultConfigWithUniqueStoreImplementation() { + this.contextRunner + .withClassLoader(new HideClassesClassLoader( + HazelcastSessionRepository.class, + JdbcOperationsSessionRepository.class)) + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .run(validateSpringSessionUsesRedis("spring:session:event:created:", RedisFlushMode.ON_SAVE)); } 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 2fb7db9d851..cae84c79de3 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 @@ -51,13 +51,13 @@ public class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurat .withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class)); @Test - public void contextFailsIfStoreTypeNotSet() { + public void contextFailsIfMultipleStoresAreAvailable() { this.contextRunner.run((context) -> { assertThat(context).hasFailed(); assertThat(context).getFailure() - .hasMessageContaining("No Spring Session store is configured"); - assertThat(context).getFailure() - .hasMessageContaining("set the 'spring.session.store-type' property"); + .hasCauseInstanceOf(NonUniqueSessionRepositoryException.class); + assertThat(context).getFailure().hasMessageContaining( + "Multiple session repository candidates are available"); }); } @@ -67,11 +67,11 @@ public class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurat .run((context) -> { assertThat(context).hasFailed(); assertThat(context).getFailure() - .isInstanceOf(BeanCreationException.class); + .hasCauseInstanceOf(SessionRepositoryUnavailableException.class); assertThat(context).getFailure().hasMessageContaining( "No session repository could be auto-configured"); - assertThat(context).getFailure() - .hasMessageContaining("session store type is 'jdbc'"); + assertThat(context).getFailure().hasMessageContaining( + "session store type is 'jdbc'"); }); } @@ -86,16 +86,17 @@ public class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurat public void backOffIfSessionRepositoryIsPresent() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) .withPropertyValues("spring.session.store-type=redis").run((context) -> { - MapSessionRepository repository = validateSessionRepository(context, - MapSessionRepository.class); - assertThat(context).getBean("mySessionRepository") - .isSameAs(repository); - }); + MapSessionRepository repository = validateSessionRepository(context, + MapSessionRepository.class); + assertThat(context).getBean("mySessionRepository") + .isSameAs(repository); + }); } @Test public void springSessionTimeoutIsNotAValidProperty() { - this.contextRunner.withPropertyValues("spring.session.timeout=3000") + this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) + .withPropertyValues("spring.session.timeout=3000") .run((context) -> { assertThat(context).hasFailed(); assertThat(context).getFailure() diff --git a/spring-boot-samples/spring-boot-sample-session-redis/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-session-redis/src/main/resources/application.properties index ae5188a80a9..1c91264a741 100644 --- a/spring-boot-samples/spring-boot-sample-session-redis/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-session-redis/src/main/resources/application.properties @@ -1,2 +1 @@ -spring.session.store-type=redis server.session.timeout=5 diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/HideClassesClassLoader.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/HideClassesClassLoader.java new file mode 100644 index 00000000000..abf4e026d41 --- /dev/null +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/HideClassesClassLoader.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2017 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.test.context; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Test {@link URLClassLoader} that hides configurable classes. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class HideClassesClassLoader extends URLClassLoader { + + private final Class[] hiddenClasses; + + public HideClassesClassLoader(Class... hiddenClasses) { + super(new URL[0], HideClassesClassLoader.class.getClassLoader()); + this.hiddenClasses = hiddenClasses; + } + + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + for (Class hiddenClass : this.hiddenClasses) { + if (name.equals(hiddenClass.getName())) { + throw new ClassNotFoundException(); + } + } + return super.loadClass(name, resolve); + } + +}