From 4670cc779548677c5286fa5f1a5beee9b08a61eb Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 13 Sep 2017 11:20:06 +0200 Subject: [PATCH] Relax use of spring.session.store-type This commit makes the "spring.session.store-type" property optional, adding an additional check when it is not present that validates only one supported implementation is available on the classpath. As Spring Session has been modularized, the chance that multiple implementations are available on the classpath are lower. When only one implementation is present, we attempt to auto-configure it. When more than one implementation is present and no session store is configured, a NonUniqueSessionRepositoryException is thrown. Closes gh-9863 --- .../NonUniqueSessionRepositoryException.java | 48 +++++++++ ...niqueSessionRepositoryFailureAnalyzer.java | 49 +++++++++ .../session/SessionAutoConfiguration.java | 61 +++++++++-- .../session/SessionCondition.java | 4 +- ...SessionRepositoryUnavailableException.java | 40 +++++++ .../main/resources/META-INF/spring.factories | 3 +- ...SessionRepositoryFailureAnalyzerTests.java | 64 +++++++++++ ...essionAutoConfigurationHazelcastTests.java | 31 ++++-- ...sionAutoConfigurationIntegrationTests.java | 101 ++++++++++++++++++ .../SessionAutoConfigurationJdbcTests.java | 42 ++++++-- .../SessionAutoConfigurationRedisTests.java | 18 +++- .../SessionAutoConfigurationTests.java | 27 ++--- .../src/main/resources/application.properties | 1 - .../test/context/HideClassesClassLoader.java | 48 +++++++++ 14 files changed, 493 insertions(+), 44 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryException.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryUnavailableException.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java create mode 100644 spring-boot-test/src/main/java/org/springframework/boot/test/context/HideClassesClassLoader.java 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); + } + +}