From de007840a8908f3089f0644ef0a6e93c8a7e56fc Mon Sep 17 00:00:00 2001 From: Tommy Ludwig Date: Tue, 26 Jan 2016 22:29:30 +0900 Subject: [PATCH] Spring Session auto-configuration expansion This implementation was inspired in large part by the cache auto-configuration. In addition to the originally supported Redis, now Hazelcast, an in-memory map, as well as a no-op option are supported. It should be easy to extend this to include additional data stores in the future. Closes gh-3811 --- .../HazelcastSessionConfiguration.java | 62 ++++++++++++++++ .../session/NoOpSessionConfiguration.java | 35 ++++++++++ .../session/RedisSessionConfiguration.java | 63 +++++++++++++++++ .../session/SessionAutoConfiguration.java | 70 +++++++++---------- .../session/SessionCondition.java | 50 +++++++++++++ .../session/SessionProperties.java | 57 +++++++++++++++ .../session/SessionStoreMappings.java | 62 ++++++++++++++++ .../session/SessionStoreType.java | 47 +++++++++++++ .../session/SimpleSessionConfiguration.java | 51 ++++++++++++++ 9 files changed, 459 insertions(+), 38 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NoOpSessionConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreMappings.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreType.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SimpleSessionConfiguration.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java new file mode 100644 index 00000000000..a59310fabe7 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java @@ -0,0 +1,62 @@ +/* + * 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 com.hazelcast.core.HazelcastInstance; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.MapSessionRepository; +import org.springframework.session.SessionRepository; +import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession; +import org.springframework.session.hazelcast.config.annotation.web.http.HazelcastHttpSessionConfiguration; + +/** + * Hazelcast backed session auto-configuration. + * + * @author Tommy Ludwig + * @since 1.4.0 + */ +@Configuration +@ConditionalOnClass({ HazelcastInstance.class, HazelcastHttpSessionConfiguration.class }) +@ConditionalOnMissingBean({ SessionRepository.class, HazelcastHttpSessionConfiguration.class }) +@EnableHazelcastHttpSession +@Conditional(SessionCondition.class) +class HazelcastSessionConfiguration { + + private final ServerProperties serverProperties; + + private final MapSessionRepository sessionRepository; + + HazelcastSessionConfiguration(ServerProperties serverProperties, MapSessionRepository sessionRepository) { + this.serverProperties = serverProperties; + this.sessionRepository = sessionRepository; + } + + @PostConstruct + public void applyConfigurationProperties() { + Integer timeout = this.serverProperties.getSession().getTimeout(); + if (timeout != null) { + this.sessionRepository.setDefaultMaxInactiveInterval(timeout); + } + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NoOpSessionConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NoOpSessionConfiguration.java new file mode 100644 index 00000000000..581ac958c07 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NoOpSessionConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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 org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.SessionRepository; + +/** + * No-op session configuration used to disable Spring Session auto configuration via the + * environment. + * + * @author Tommy Ludwig + * @since 1.4.0 + */ +@Configuration +@ConditionalOnMissingBean(SessionRepository.class) +@Conditional(SessionCondition.class) +class NoOpSessionConfiguration { +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java new file mode 100644 index 00000000000..402f2ed05ee --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java @@ -0,0 +1,63 @@ +/* + * 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 org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.session.SessionRepository; +import org.springframework.session.data.redis.RedisOperationsSessionRepository; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; +import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration; + +/** + * Redis backed session auto-configuration. + * + * @author Andy Wilkinson + * @author Tommy Ludwig + * @since 1.4.0 + */ +@Configuration +@ConditionalOnClass(RedisConnectionFactory.class) +@ConditionalOnMissingBean({ SessionRepository.class, RedisHttpSessionConfiguration.class }) +@EnableRedisHttpSession +@Conditional(SessionCondition.class) +class RedisSessionConfiguration { + + private final ServerProperties serverProperties; + + private final RedisOperationsSessionRepository sessionRepository; + + RedisSessionConfiguration(ServerProperties serverProperties, RedisOperationsSessionRepository sessionRepository) { + this.serverProperties = serverProperties; + this.sessionRepository = sessionRepository; + } + + @PostConstruct + public void applyConfigurationProperties() { + Integer timeout = this.serverProperties.getSession().getTimeout(); + if (timeout != null) { + this.sessionRepository.setDefaultMaxInactiveInterval(timeout); + } + } + +} 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 b26de37a33a..a3cb962a98b 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 @@ -23,64 +23,58 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; +import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration.SessionConfigurationImportSelector; import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.session.Session; -import org.springframework.session.data.redis.RedisOperationsSessionRepository; -import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; -import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration; +import org.springframework.session.SessionRepository; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Session. * * @author Andy Wilkinson + * @author Tommy Ludwig * @since 1.3.0 */ @Configuration @ConditionalOnClass(Session.class) -@AutoConfigureAfter(RedisAutoConfiguration.class) +@ConditionalOnWebApplication +@ConditionalOnMissingBean(SessionRepository.class) +@AutoConfigureAfter({ HazelcastAutoConfiguration.class, RedisAutoConfiguration.class }) +@Import(SessionConfigurationImportSelector.class) public class SessionAutoConfiguration { - @EnableConfigurationProperties - @ConditionalOnClass(RedisConnectionFactory.class) - @ConditionalOnWebApplication - @ConditionalOnMissingBean(RedisHttpSessionConfiguration.class) - @EnableRedisHttpSession @Configuration - public static class SessionRedisHttpConfiguration { + @ConditionalOnMissingBean(value = ServerProperties.class, search = SearchStrategy.CURRENT) + // Just in case user switches off ServerPropertiesAutoConfiguration + public static class ServerPropertiesConfiguration { - private final ServerProperties serverProperties; - - private final RedisOperationsSessionRepository sessionRepository; - - public SessionRedisHttpConfiguration(ServerProperties serverProperties, - RedisOperationsSessionRepository sessionRepository) { - this.serverProperties = serverProperties; - this.sessionRepository = sessionRepository; - applyConfigurationProperties(); + @Bean + // Use the same bean name as the default one for any old webapp + public ServerProperties serverProperties() { + return new ServerProperties(); } - private void applyConfigurationProperties() { - Integer timeout = this.serverProperties.getSession().getTimeout(); - if (timeout != null) { - this.sessionRepository.setDefaultMaxInactiveInterval(timeout); + } + + /** + * {@link ImportSelector} to add {@link SessionStoreType} configuration classes. + */ + static class SessionConfigurationImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + SessionStoreType[] types = SessionStoreType.values(); + String[] imports = new String[types.length]; + for (int i = 0; i < types.length; i++) { + imports[i] = SessionStoreMappings.getConfigurationClass(types[i]); } - } - - @Configuration - @ConditionalOnMissingBean(value = ServerProperties.class, search = SearchStrategy.CURRENT) - // Just in case user switches off ServerPropertiesAutoConfiguration - public static class ServerPropertiesConfiguration { - - @Bean - // Use the same bean name as the default one for any old webapp - public ServerProperties serverProperties() { - return new ServerProperties(); - } - + return imports; } } 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 new file mode 100644 index 00000000000..a3a3261a8d3 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionCondition.java @@ -0,0 +1,50 @@ +/* + * 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 org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; + +/** + * General condition used by all session auto-configuration classes. + * + * @author Tommy Ludwig + */ +class SessionCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( + context.getEnvironment(), "spring.session.store."); + if (!resolver.containsProperty("type")) { + return ConditionOutcome.match("Automatic session store type"); + } + SessionStoreType sessionStoreType = SessionStoreMappings + .getType(((AnnotationMetadata) metadata).getClassName()); + String value = resolver.getProperty("type").replace("-", "_").toUpperCase(); + if (value.equals(sessionStoreType.name())) { + return ConditionOutcome.match("Session store type " + sessionStoreType); + } + return ConditionOutcome.noMatch("Session store type " + value); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java new file mode 100644 index 00000000000..66b5eea50d2 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java @@ -0,0 +1,57 @@ +/* + * 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 org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties for configuring Spring Session's auto-configuration. + * + * @author Tommy Ludwig + * @since 1.4.0 + */ +@ConfigurationProperties("spring.session") +public class SessionProperties { + + private Store store; + + public Store getStore() { + return this.store; + } + + public void setStore(Store store) { + this.store = store; + } + + /** + * Session store-specific properties. + */ + public static class Store { + /** + * Session data store type, auto-detected according to the environment by default. + */ + private SessionStoreType type; + + public SessionStoreType getType() { + return this.type; + } + + public void setType(SessionStoreType type) { + this.type = type; + } + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreMappings.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreMappings.java new file mode 100644 index 00000000000..50078464af4 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreMappings.java @@ -0,0 +1,62 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Mappings between {@link SessionStoreType} and {@code @Configuration}. + * + * @author Tommy Ludwig + */ +final class SessionStoreMappings { + + private SessionStoreMappings() { + } + + private static final Map> MAPPINGS; + + static { + Map> mappings = new HashMap>(); + mappings.put(SessionStoreType.REDIS, RedisSessionConfiguration.class); + mappings.put(SessionStoreType.HAZELCAST, HazelcastSessionConfiguration.class); + mappings.put(SessionStoreType.SIMPLE, SimpleSessionConfiguration.class); + mappings.put(SessionStoreType.NONE, NoOpSessionConfiguration.class); + MAPPINGS = Collections.unmodifiableMap(mappings); + } + + public static String getConfigurationClass(SessionStoreType sessionStoreType) { + Class configurationClass = MAPPINGS.get(sessionStoreType); + Assert.state(configurationClass != null, + "Unknown session store type " + sessionStoreType); + return configurationClass.getName(); + } + + public static SessionStoreType getType(String configurationClassName) { + for (Map.Entry> entry : MAPPINGS.entrySet()) { + if (entry.getValue().getName().equals(configurationClassName)) { + return entry.getKey(); + } + } + throw new IllegalStateException( + "Unknown configuration class " + configurationClassName); + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreType.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreType.java new file mode 100644 index 00000000000..ce508c58969 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionStoreType.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * Supported Spring Session data store types. + * + * @author Tommy Ludwig + * @since 1.4.0 + */ +public enum SessionStoreType { + + /** + * Redis backed sessions. + */ + REDIS, + + /** + * Hazelcast backed sessions. + */ + HAZELCAST, + + /** + * Simple in-memory map of sessions. + */ + SIMPLE, + + /** + * No session datastore. + */ + NONE; + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SimpleSessionConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SimpleSessionConfiguration.java new file mode 100644 index 00000000000..43347827877 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SimpleSessionConfiguration.java @@ -0,0 +1,51 @@ +/* + * 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 org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.ExpiringSession; +import org.springframework.session.MapSessionRepository; +import org.springframework.session.SessionRepository; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; + +/** + * In-memory session configuration, intended as a fallback. + * + * @author Tommy Ludwig + * @since 1.4.0 + */ +@Configuration +@ConditionalOnMissingBean(SessionRepository.class) +@EnableSpringHttpSession +@Conditional(SessionCondition.class) +class SimpleSessionConfiguration { + + @Bean + public SessionRepository sessionRepository(ServerProperties serverProperties) { + MapSessionRepository sessionRepository = new MapSessionRepository(); + + Integer timeout = serverProperties.getSession().getTimeout(); + if (serverProperties.getSession().getTimeout() != null) { + sessionRepository.setDefaultMaxInactiveInterval(timeout); + } + return sessionRepository; + } +}