From 8eab0687786f7cd9bac85d8cfa4c939e705ea097 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Tue, 16 Aug 2022 17:09:09 +0200 Subject: [PATCH 1/2] Revert "Use simple Redis repository by default" This reverts commit 8582b970 See gh-2122 --- .../RedisIndexedSessionRepositoryITests.java | 2 +- .../redis/RedisSessionRepositoryITests.java | 15 +- ...ttpSessionExpireSessionDestroyedTests.java | 2 +- ...isListenerContainerTaskExecutorITests.java | 2 +- .../web/http/EnableRedisHttpSession.java | 22 +- .../http/RedisHttpSessionConfiguration.java | 202 +++++++- ...RedisHttpSessionConfigurationSelector.java | 46 -- .../RedisIndexedHttpSessionConfiguration.java | 348 -------------- ...KeyspaceNotificationsInitializerTests.java | 4 +- ...disHttpSessionConfigurationMockTests.java} | 4 +- ...igurationOverrideSessionTaskExecutor.java} | 4 +- ...gurationOverrideSessionTaskExecutors.java} | 4 +- .../RedisHttpSessionConfigurationTests.java | 114 ++++- ...sIndexedHttpSessionConfigurationTests.java | 442 ------------------ .../HttpSessionListenerXmlTests-context.xml | 2 +- 15 files changed, 322 insertions(+), 891 deletions(-) delete mode 100644 spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationSelector.java delete mode 100644 spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisIndexedHttpSessionConfigurationMockTests.java => RedisHttpSessionConfigurationMockTests.java} (95%) rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java => RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java} (96%) rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java => RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java} (96%) delete mode 100644 spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java index 501b2d221..243104fdc 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java @@ -691,7 +691,7 @@ private String getChangedSecurityName() { } @Configuration - @EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests", enableIndexingAndEvents = true) + @EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests") static class Config extends BaseConfig { @Bean diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java index 6ea5963a2..afe396965 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java @@ -26,10 +26,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.session.MapSession; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.data.redis.RedisSessionRepository.RedisSession; -import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; @@ -220,9 +223,17 @@ private static void updateSession(RedisSession session, Instant lastAccessedTime } @Configuration - @EnableRedisHttpSession + @EnableSpringHttpSession static class Config extends BaseConfig { + @Bean + RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.afterPropertiesSet(); + return new RedisSessionRepository(redisTemplate); + } + } } diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java index 10fd1415f..bc91c15fc 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java @@ -113,7 +113,7 @@ void setLock(Object lock) { } @Configuration - @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1, enableIndexingAndEvents = true) + @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1) static class Config extends BaseConfig { @Bean diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java index e70f159c2..8176d2a16 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java @@ -101,7 +101,7 @@ boolean taskDispatched() throws InterruptedException { } @Configuration - @EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests", enableIndexingAndEvents = true) + @EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests") static class Config extends BaseConfig { @Bean diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java index 4df427778..125ab8aa1 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java @@ -32,7 +32,6 @@ import org.springframework.session.SessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.data.redis.RedisIndexedSessionRepository; -import org.springframework.session.data.redis.RedisSessionRepository; import org.springframework.session.web.http.SessionRepositoryFilter; /** @@ -54,8 +53,7 @@ * } * * - * More advanced configurations can extend {@link RedisHttpSessionConfiguration} or - * {@link RedisIndexedHttpSessionConfiguration} instead. + * More advanced configurations can extend {@link RedisHttpSessionConfiguration} instead. * * @author Rob Winch * @author Vedran Pavic @@ -65,7 +63,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented -@Import(RedisHttpSessionConfigurationSelector.class) +@Import(RedisHttpSessionConfiguration.class) @Configuration(proxyBeanMethods = false) public @interface EnableRedisHttpSession { @@ -100,6 +98,13 @@ */ FlushMode flushMode() default FlushMode.ON_SAVE; + /** + * The cron expression for expired session cleanup job. By default runs every minute. + * @return the session cleanup cron expression + * @since 2.0.0 + */ + String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; + /** * Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which * only saves changes made to session. @@ -108,13 +113,4 @@ */ SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE; - /** - * Indicate whether the {@link SessionRepository} should publish session events and - * support fetching sessions by index. If true, a - * {@link RedisIndexedSessionRepository} will be used in place of - * {@link RedisSessionRepository}. This will result in slower performance. - * @return true if indexing and events should be enabled, false otherwise - */ - boolean enableIndexingAndEvents() default false; - } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java index 923a382dc..49dec20c6 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java @@ -16,34 +16,54 @@ package org.springframework.session.data.redis.config.annotation.web.http; -import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; import java.util.stream.Collectors; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.session.FlushMode; +import org.springframework.session.IndexResolver; import org.springframework.session.MapSession; import org.springframework.session.SaveMode; +import org.springframework.session.Session; import org.springframework.session.config.SessionRepositoryCustomizer; import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; -import org.springframework.session.data.redis.RedisSessionRepository; +import org.springframework.session.data.redis.RedisIndexedSessionRepository; +import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction; +import org.springframework.session.data.redis.config.ConfigureRedisAction; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; import org.springframework.session.web.http.SessionRepositoryFilter; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -62,39 +82,86 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { + static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; + private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; - private String redisNamespace = RedisSessionRepository.DEFAULT_KEY_NAMESPACE; + private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE; private FlushMode flushMode = FlushMode.ON_SAVE; private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; + private String cleanupCron = DEFAULT_CLEANUP_CRON; + + private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); + private RedisConnectionFactory redisConnectionFactory; + private IndexResolver indexResolver; + private RedisSerializer defaultRedisSerializer; - private List> sessionRepositoryCustomizers; + private ApplicationEventPublisher applicationEventPublisher; + + private Executor redisTaskExecutor; + + private Executor redisSubscriptionExecutor; + + private List> sessionRepositoryCustomizers; private ClassLoader classLoader; private StringValueResolver embeddedValueResolver; @Bean - public RedisSessionRepository sessionRepository() { - RedisTemplate redisTemplate = createRedisTemplate(); - RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate); - sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(this.maxInactiveIntervalInSeconds)); + public RedisIndexedSessionRepository sessionRepository() { + RedisTemplate redisTemplate = createRedisTemplate(); + RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate); + sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); + if (this.indexResolver != null) { + sessionRepository.setIndexResolver(this.indexResolver); + } + if (this.defaultRedisSerializer != null) { + sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); + } + sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); if (StringUtils.hasText(this.redisNamespace)) { sessionRepository.setRedisKeyNamespace(this.redisNamespace); } sessionRepository.setFlushMode(this.flushMode); sessionRepository.setSaveMode(this.saveMode); + int database = resolveDatabase(); + sessionRepository.setDatabase(database); this.sessionRepositoryCustomizers .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository)); return sessionRepository; } + @Bean + public RedisMessageListenerContainer springSessionRedisMessageListenerContainer( + RedisIndexedSessionRepository sessionRepository) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(this.redisConnectionFactory); + if (this.redisTaskExecutor != null) { + container.setTaskExecutor(this.redisTaskExecutor); + } + if (this.redisSubscriptionExecutor != null) { + container.setSubscriptionExecutor(this.redisSubscriptionExecutor); + } + container.addMessageListener(sessionRepository, + Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()), + new ChannelTopic(sessionRepository.getSessionExpiredChannel()))); + container.addMessageListener(sessionRepository, + Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*"))); + return container; + } + + @Bean + public InitializingBean enableRedisKeyspaceNotificationsInitializer() { + return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction); + } + public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) { this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds; } @@ -112,6 +179,20 @@ public void setSaveMode(SaveMode saveMode) { this.saveMode = saveMode; } + public void setCleanupCron(String cleanupCron) { + this.cleanupCron = cleanupCron; + } + + /** + * Sets the action to perform for configuring Redis. + * @param configureRedisAction the configureRedis to set. The default is + * {@link ConfigureNotifyKeyspaceEventsAction}. + */ + @Autowired(required = false) + public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) { + this.configureRedisAction = configureRedisAction; + } + @Autowired public void setRedisConnectionFactory( @SpringSessionRedisConnectionFactory ObjectProvider springSessionRedisConnectionFactory, @@ -129,9 +210,31 @@ public void setDefaultRedisSerializer(RedisSerializer defaultRedisSerial this.defaultRedisSerializer = defaultRedisSerializer; } + @Autowired + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Autowired(required = false) + public void setIndexResolver(IndexResolver indexResolver) { + this.indexResolver = indexResolver; + } + + @Autowired(required = false) + @Qualifier("springSessionRedisTaskExecutor") + public void setRedisTaskExecutor(Executor redisTaskExecutor) { + this.redisTaskExecutor = redisTaskExecutor; + } + + @Autowired(required = false) + @Qualifier("springSessionRedisSubscriptionExecutor") + public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) { + this.redisSubscriptionExecutor = redisSubscriptionExecutor; + } + @Autowired(required = false) public void setSessionRepositoryCustomizer( - ObjectProvider> sessionRepositoryCustomizers) { + ObjectProvider> sessionRepositoryCustomizers) { this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList()); } @@ -157,10 +260,14 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { } this.flushMode = attributes.getEnum("flushMode"); this.saveMode = attributes.getEnum("saveMode"); + String cleanupCron = attributes.getString("cleanupCron"); + if (StringUtils.hasText(cleanupCron)) { + this.cleanupCron = cleanupCron; + } } - private RedisTemplate createRedisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); + private RedisTemplate createRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); if (this.defaultRedisSerializer != null) { @@ -172,4 +279,77 @@ private RedisTemplate createRedisTemplate() { return redisTemplate; } + private int resolveDatabase() { + if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null) + && this.redisConnectionFactory instanceof LettuceConnectionFactory) { + return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase(); + } + if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null) + && this.redisConnectionFactory instanceof JedisConnectionFactory) { + return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase(); + } + return RedisIndexedSessionRepository.DEFAULT_DATABASE; + } + + /** + * Ensures that Redis is configured to send keyspace notifications. This is important + * to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents. + * Without the SessionDestroyedEvent resources may not get cleaned up properly. For + * example, the mapping of the Session to WebSocket connections may not get cleaned + * up. + */ + static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean { + + private final RedisConnectionFactory connectionFactory; + + private ConfigureRedisAction configure; + + EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory, + ConfigureRedisAction configure) { + this.connectionFactory = connectionFactory; + this.configure = configure; + } + + @Override + public void afterPropertiesSet() { + if (this.configure == ConfigureRedisAction.NO_OP) { + return; + } + RedisConnection connection = this.connectionFactory.getConnection(); + try { + this.configure.configure(connection); + } + finally { + try { + connection.close(); + } + catch (Exception ex) { + LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex); + } + } + } + + } + + /** + * Configuration of scheduled job for cleaning up expired sessions. + */ + @EnableScheduling + @Configuration(proxyBeanMethods = false) + class SessionCleanupConfiguration implements SchedulingConfigurer { + + private final RedisIndexedSessionRepository sessionRepository; + + SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) { + this.sessionRepository = sessionRepository; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions, + RedisHttpSessionConfiguration.this.cleanupCron); + } + + } + } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationSelector.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationSelector.java deleted file mode 100644 index 704e26286..000000000 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationSelector.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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.session.data.redis.config.annotation.web.http; - -import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.type.AnnotationMetadata; - -/** - * Dynamically determines which session repository configuration to include using the - * {@link EnableRedisHttpSession} annotation. - * - * @author Eleftheria Stein - * @since 3.0 - */ -final class RedisHttpSessionConfigurationSelector implements ImportSelector { - - @Override - public String[] selectImports(AnnotationMetadata importMetadata) { - if (!importMetadata.hasAnnotation(EnableRedisHttpSession.class.getName())) { - return new String[0]; - } - EnableRedisHttpSession annotation = importMetadata.getAnnotations().get(EnableRedisHttpSession.class) - .synthesize(); - if (annotation.enableIndexingAndEvents()) { - return new String[] { RedisIndexedHttpSessionConfiguration.class.getName() }; - } - else { - return new String[] { RedisHttpSessionConfiguration.class.getName() }; - } - } - -} diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java deleted file mode 100644 index 67614d50b..000000000 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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.session.data.redis.config.annotation.web.http; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.stream.Collectors; - -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.EmbeddedValueResolverAware; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportAware; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.listener.ChannelTopic; -import org.springframework.data.redis.listener.PatternTopic; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.session.FlushMode; -import org.springframework.session.IndexResolver; -import org.springframework.session.MapSession; -import org.springframework.session.SaveMode; -import org.springframework.session.Session; -import org.springframework.session.config.SessionRepositoryCustomizer; -import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; -import org.springframework.session.data.redis.RedisIndexedSessionRepository; -import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction; -import org.springframework.session.data.redis.config.ConfigureRedisAction; -import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; -import org.springframework.session.web.http.SessionRepositoryFilter; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.util.StringValueResolver; - -/** - * Exposes the {@link SessionRepositoryFilter} as a bean named - * {@code springSessionRepositoryFilter}. In order to use this a single - * {@link RedisConnectionFactory} must be exposed as a Bean. - * - * @author Eleftheria Stein - * @since 3.0 - */ -@Configuration(proxyBeanMethods = false) -public class RedisIndexedHttpSessionConfiguration extends SpringHttpSessionConfiguration - implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { - - static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; - - private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; - - private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE; - - private FlushMode flushMode = FlushMode.ON_SAVE; - - private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; - - private String cleanupCron = DEFAULT_CLEANUP_CRON; - - private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); - - private RedisConnectionFactory redisConnectionFactory; - - private IndexResolver indexResolver; - - private RedisSerializer defaultRedisSerializer; - - private ApplicationEventPublisher applicationEventPublisher; - - private Executor redisTaskExecutor; - - private Executor redisSubscriptionExecutor; - - private List> sessionRepositoryCustomizers; - - private ClassLoader classLoader; - - private StringValueResolver embeddedValueResolver; - - @Bean - public RedisIndexedSessionRepository sessionRepository() { - RedisTemplate redisTemplate = createRedisTemplate(); - RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate); - sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); - if (this.indexResolver != null) { - sessionRepository.setIndexResolver(this.indexResolver); - } - if (this.defaultRedisSerializer != null) { - sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); - } - sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); - if (StringUtils.hasText(this.redisNamespace)) { - sessionRepository.setRedisKeyNamespace(this.redisNamespace); - } - sessionRepository.setFlushMode(this.flushMode); - sessionRepository.setSaveMode(this.saveMode); - int database = resolveDatabase(); - sessionRepository.setDatabase(database); - this.sessionRepositoryCustomizers - .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository)); - return sessionRepository; - } - - @Bean - public RedisMessageListenerContainer springSessionRedisMessageListenerContainer( - RedisIndexedSessionRepository sessionRepository) { - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(this.redisConnectionFactory); - if (this.redisTaskExecutor != null) { - container.setTaskExecutor(this.redisTaskExecutor); - } - if (this.redisSubscriptionExecutor != null) { - container.setSubscriptionExecutor(this.redisSubscriptionExecutor); - } - container.addMessageListener(sessionRepository, - Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()), - new ChannelTopic(sessionRepository.getSessionExpiredChannel()))); - container.addMessageListener(sessionRepository, - Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*"))); - return container; - } - - @Bean - public InitializingBean enableRedisKeyspaceNotificationsInitializer() { - return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction); - } - - public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) { - this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds; - } - - public void setRedisNamespace(String namespace) { - this.redisNamespace = namespace; - } - - public void setFlushMode(FlushMode flushMode) { - Assert.notNull(flushMode, "flushMode cannot be null"); - this.flushMode = flushMode; - } - - public void setSaveMode(SaveMode saveMode) { - this.saveMode = saveMode; - } - - public void setCleanupCron(String cleanupCron) { - this.cleanupCron = cleanupCron; - } - - /** - * Sets the action to perform for configuring Redis. - * @param configureRedisAction the configureRedis to set. The default is - * {@link ConfigureNotifyKeyspaceEventsAction}. - */ - @Autowired(required = false) - public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) { - this.configureRedisAction = configureRedisAction; - } - - @Autowired - public void setRedisConnectionFactory( - @SpringSessionRedisConnectionFactory ObjectProvider springSessionRedisConnectionFactory, - ObjectProvider redisConnectionFactory) { - RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable(); - if (redisConnectionFactoryToUse == null) { - redisConnectionFactoryToUse = redisConnectionFactory.getObject(); - } - this.redisConnectionFactory = redisConnectionFactoryToUse; - } - - @Autowired(required = false) - @Qualifier("springSessionDefaultRedisSerializer") - public void setDefaultRedisSerializer(RedisSerializer defaultRedisSerializer) { - this.defaultRedisSerializer = defaultRedisSerializer; - } - - @Autowired - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - @Autowired(required = false) - public void setIndexResolver(IndexResolver indexResolver) { - this.indexResolver = indexResolver; - } - - @Autowired(required = false) - @Qualifier("springSessionRedisTaskExecutor") - public void setRedisTaskExecutor(Executor redisTaskExecutor) { - this.redisTaskExecutor = redisTaskExecutor; - } - - @Autowired(required = false) - @Qualifier("springSessionRedisSubscriptionExecutor") - public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) { - this.redisSubscriptionExecutor = redisSubscriptionExecutor; - } - - @Autowired(required = false) - public void setSessionRepositoryCustomizer( - ObjectProvider> sessionRepositoryCustomizers) { - this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList()); - } - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.embeddedValueResolver = resolver; - } - - @Override - public void setImportMetadata(AnnotationMetadata importMetadata) { - Map attributeMap = importMetadata - .getAnnotationAttributes(EnableRedisHttpSession.class.getName()); - AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap); - this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds"); - String redisNamespaceValue = attributes.getString("redisNamespace"); - if (StringUtils.hasText(redisNamespaceValue)) { - this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue); - } - this.flushMode = attributes.getEnum("flushMode"); - this.saveMode = attributes.getEnum("saveMode"); - } - - private RedisTemplate createRedisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - if (this.defaultRedisSerializer != null) { - redisTemplate.setDefaultSerializer(this.defaultRedisSerializer); - } - redisTemplate.setConnectionFactory(this.redisConnectionFactory); - redisTemplate.setBeanClassLoader(this.classLoader); - redisTemplate.afterPropertiesSet(); - return redisTemplate; - } - - private int resolveDatabase() { - if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null) - && this.redisConnectionFactory instanceof LettuceConnectionFactory) { - return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase(); - } - if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null) - && this.redisConnectionFactory instanceof JedisConnectionFactory) { - return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase(); - } - return RedisIndexedSessionRepository.DEFAULT_DATABASE; - } - - /** - * Ensures that Redis is configured to send keyspace notifications. This is important - * to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents. - * Without the SessionDestroyedEvent resources may not get cleaned up properly. For - * example, the mapping of the Session to WebSocket connections may not get cleaned - * up. - */ - static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean { - - private final RedisConnectionFactory connectionFactory; - - private ConfigureRedisAction configure; - - EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory, - ConfigureRedisAction configure) { - this.connectionFactory = connectionFactory; - this.configure = configure; - } - - @Override - public void afterPropertiesSet() { - if (this.configure == ConfigureRedisAction.NO_OP) { - return; - } - RedisConnection connection = this.connectionFactory.getConnection(); - try { - this.configure.configure(connection); - } - finally { - try { - connection.close(); - } - catch (Exception ex) { - LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex); - } - } - } - - } - - /** - * Configuration of scheduled job for cleaning up expired sessions. - */ - @EnableScheduling - @Configuration(proxyBeanMethods = false) - class SessionCleanupConfiguration implements SchedulingConfigurer { - - private final RedisIndexedSessionRepository sessionRepository; - - SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) { - this.sessionRepository = sessionRepository; - } - - @Override - public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { - taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions, - RedisIndexedHttpSessionConfiguration.this.cleanupCron); - } - - } - -} diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java index 9729400b4..80c59f85c 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java @@ -55,14 +55,14 @@ class EnableRedisKeyspaceNotificationsInitializerTests { @Captor ArgumentCaptor options; - private RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer; + private RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer; @BeforeEach void setup() { given(this.connectionFactory.getConnection()).willReturn(this.connection); given(this.connection.serverCommands()).willReturn(this.commands); - this.initializer = new RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer( + this.initializer = new RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer( this.connectionFactory, new ConfigureNotifyKeyspaceEventsAction()); } diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationMockTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationMockTests.java similarity index 95% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationMockTests.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationMockTests.java index 7e2ca4df5..ab1edae69 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationMockTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationMockTests.java @@ -25,7 +25,7 @@ import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.session.data.redis.config.ConfigureRedisAction; -import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer; +import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import static org.mockito.BDDMockito.given; @@ -35,7 +35,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) -class RedisIndexedHttpSessionConfigurationMockTests { +class RedisHttpSessionConfigurationMockTests { @Mock(lenient = true) RedisConnectionFactory factory; diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java similarity index 96% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java index 1c346bd61..cbd14b7b2 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java @@ -49,7 +49,7 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor { +class RedisHttpSessionConfigurationOverrideSessionTaskExecutor { @Autowired RedisMessageListenerContainer redisMessageListenerContainer; @@ -62,7 +62,7 @@ void overrideSessionTaskExecutor() { verify(this.springSessionRedisTaskExecutor, times(1)).execute(any(Runnable.class)); } - @EnableRedisHttpSession(enableIndexingAndEvents = true) + @EnableRedisHttpSession @Configuration static class Config { diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java similarity index 96% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java index 50afca014..c79db725b 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java @@ -51,7 +51,7 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors { +class RedisHttpSessionConfigurationOverrideSessionTaskExecutors { @Autowired RedisMessageListenerContainer redisMessageListenerContainer; @@ -68,7 +68,7 @@ void overrideSessionTaskExecutors() { verify(this.springSessionRedisTaskExecutor, never()).execute(any(Runnable.class)); } - @EnableRedisHttpSession(enableIndexingAndEvents = true) + @EnableRedisHttpSession @Configuration static class Config { diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java index d2a924045..75fdb1161 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java @@ -16,7 +16,7 @@ package org.springframework.session.data.redis.config.annotation.web.http; -import java.time.Duration; +import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.AfterEach; @@ -36,11 +36,14 @@ import org.springframework.data.redis.connection.RedisServerCommands; import org.springframework.data.redis.connection.SubscriptionListener; import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.mock.env.MockEnvironment; import org.springframework.session.FlushMode; +import org.springframework.session.IndexResolver; import org.springframework.session.SaveMode; +import org.springframework.session.Session; import org.springframework.session.config.SessionRepositoryCustomizer; -import org.springframework.session.data.redis.RedisSessionRepository; +import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; import org.springframework.test.util.ReflectionTestUtils; @@ -60,7 +63,9 @@ */ class RedisHttpSessionConfigurationTests { - private static final Duration MAX_INACTIVE_INTERVAL_DURATION = Duration.ofSeconds(600); + private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600; + + private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *"; private AnnotationConfigApplicationContext context; @@ -96,7 +101,7 @@ void resolveValueByPlaceholder() { @Test void customFlushImmediately() { registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class); - RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); assertThat(sessionRepository).isNotNull(); assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); } @@ -104,22 +109,40 @@ void customFlushImmediately() { @Test void setCustomFlushImmediately() { registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class); - RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); assertThat(sessionRepository).isNotNull(); assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); } + @Test + void customCleanupCronAnnotation() { + registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionAnnotationConfiguration.class); + + RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + assertThat(configuration).isNotNull(); + assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); + } + + @Test + void customCleanupCronSetter() { + registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class); + + RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + assertThat(configuration).isNotNull(); + assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); + } + @Test void customSaveModeAnnotation() { registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class); - assertThat(this.context.getBean(RedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", + assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS); } @Test void customSaveModeSetter() { registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class); - assertThat(this.context.getBean(RedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", + assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", SaveMode.ALWAYS); } @@ -127,7 +150,7 @@ void customSaveModeSetter() { void qualifiedConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class); - RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory", RedisConnectionFactory.class); assertThat(repository).isNotNull(); @@ -143,7 +166,7 @@ void qualifiedConnectionFactoryRedisConfig() { void primaryConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class); - RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory", RedisConnectionFactory.class); assertThat(repository).isNotNull(); @@ -159,7 +182,7 @@ void primaryConnectionFactoryRedisConfig() { void qualifiedAndPrimaryConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class); - RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory", RedisConnectionFactory.class); assertThat(repository).isNotNull(); @@ -175,7 +198,7 @@ void qualifiedAndPrimaryConnectionFactoryRedisConfig() { void namedConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class); - RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory", RedisConnectionFactory.class); assertThat(repository).isNotNull(); @@ -195,12 +218,32 @@ void multipleConnectionFactoryRedisConfig() { .withMessageContaining("expected single matching bean but found 2"); } + @Test + void customIndexResolverConfiguration() { + registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class); + RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); + @SuppressWarnings("unchecked") + IndexResolver indexResolver = this.context.getBean(IndexResolver.class); + assertThat(repository).isNotNull(); + assertThat(indexResolver).isNotNull(); + assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver); + } + + @Test // gh-1252 + void customRedisMessageListenerContainerConfig() { + registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class); + Map beans = this.context + .getBeansOfType(RedisMessageListenerContainer.class); + assertThat(beans).hasSize(2); + assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer"); + } + @Test void sessionRepositoryCustomizer() { registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class); - RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class); + RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", - MAX_INACTIVE_INTERVAL_DURATION); + MAX_INACTIVE_INTERVAL_IN_SECONDS); } private void registerAndRefresh(Class... annotatedClasses) { @@ -266,6 +309,20 @@ static class CustomFlushImmediatelyConfiguration { } + @EnableRedisHttpSession(cleanupCron = CLEANUP_CRON_EXPRESSION) + static class CustomCleanupCronExpressionAnnotationConfiguration { + + } + + @Configuration + static class CustomCleanupCronExpressionSetterConfiguration extends RedisHttpSessionConfiguration { + + CustomCleanupCronExpressionSetterConfiguration() { + setCleanupCron(CLEANUP_CRON_EXPRESSION); + } + + } + @EnableRedisHttpSession(saveMode = SaveMode.ALWAYS) static class CustomSaveModeExpressionAnnotationConfiguration { @@ -356,20 +413,43 @@ static class CustomRedisHttpSessionConfiguration2 { } + @Configuration + @EnableRedisHttpSession + static class CustomIndexResolverConfiguration { + + @Bean + @SuppressWarnings("unchecked") + IndexResolver indexResolver() { + return mock(IndexResolver.class); + } + + } + + @Configuration + @EnableRedisHttpSession + static class CustomRedisMessageListenerContainerConfig { + + @Bean + RedisMessageListenerContainer redisMessageListenerContainer() { + return mock(RedisMessageListenerContainer.class); + } + + } + @EnableRedisHttpSession static class SessionRepositoryCustomizerConfiguration { @Bean @Order(0) - SessionRepositoryCustomizer sessionRepositoryCustomizerOne() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); + SessionRepositoryCustomizer sessionRepositoryCustomizerOne() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0); } @Bean @Order(1) - SessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { + SessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { return (sessionRepository) -> sessionRepository - .setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_DURATION); + .setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS); } } diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java deleted file mode 100644 index 7c9833c12..000000000 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java +++ /dev/null @@ -1,442 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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.session.data.redis.config.annotation.web.http; - -import java.util.Map; -import java.util.Properties; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.core.annotation.Order; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisServerCommands; -import org.springframework.data.redis.connection.SubscriptionListener; -import org.springframework.data.redis.core.RedisOperations; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.mock.env.MockEnvironment; -import org.springframework.session.FlushMode; -import org.springframework.session.IndexResolver; -import org.springframework.session.SaveMode; -import org.springframework.session.Session; -import org.springframework.session.config.SessionRepositoryCustomizer; -import org.springframework.session.data.redis.RedisIndexedSessionRepository; -import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link RedisIndexedHttpSessionConfiguration}. - */ -class RedisIndexedHttpSessionConfigurationTests { - - private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600; - - private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *"; - - private AnnotationConfigApplicationContext context; - - @BeforeEach - void before() { - this.context = new AnnotationConfigApplicationContext(); - } - - @AfterEach - void after() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - void resolveValue() { - registerAndRefresh(RedisConfig.class, CustomRedisHttpSessionConfiguration.class); - RedisIndexedHttpSessionConfiguration configuration = this.context - .getBean(RedisIndexedHttpSessionConfiguration.class); - assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("myRedisNamespace"); - } - - @Test - void resolveValueByPlaceholder() { - this.context - .setEnvironment(new MockEnvironment().withProperty("session.redis.namespace", "customRedisNamespace")); - registerAndRefresh(RedisConfig.class, PropertySourceConfiguration.class, - CustomRedisHttpSessionConfiguration2.class); - RedisIndexedHttpSessionConfiguration configuration = this.context - .getBean(RedisIndexedHttpSessionConfiguration.class); - assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("customRedisNamespace"); - } - - @Test - void customFlushImmediately() { - registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class); - RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); - assertThat(sessionRepository).isNotNull(); - assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); - } - - @Test - void setCustomFlushImmediately() { - registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class); - RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); - assertThat(sessionRepository).isNotNull(); - assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); - } - - @Test - void customCleanupCronSetter() { - registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class); - - RedisIndexedHttpSessionConfiguration configuration = this.context - .getBean(RedisIndexedHttpSessionConfiguration.class); - assertThat(configuration).isNotNull(); - assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); - } - - @Test - void customSaveModeAnnotation() { - registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class); - assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", - SaveMode.ALWAYS); - } - - @Test - void customSaveModeSetter() { - registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class); - assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", - SaveMode.ALWAYS); - } - - @Test - void qualifiedConnectionFactoryRedisConfig() { - registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class); - - RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); - RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory", - RedisConnectionFactory.class); - assertThat(repository).isNotNull(); - assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); - assertThat(redisOperations).isNotNull(); - assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) - .isEqualTo(redisConnectionFactory); - } - - @Test - void primaryConnectionFactoryRedisConfig() { - registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class); - - RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); - RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory", - RedisConnectionFactory.class); - assertThat(repository).isNotNull(); - assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); - assertThat(redisOperations).isNotNull(); - assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) - .isEqualTo(redisConnectionFactory); - } - - @Test - void qualifiedAndPrimaryConnectionFactoryRedisConfig() { - registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class); - - RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); - RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory", - RedisConnectionFactory.class); - assertThat(repository).isNotNull(); - assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); - assertThat(redisOperations).isNotNull(); - assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) - .isEqualTo(redisConnectionFactory); - } - - @Test - void namedConnectionFactoryRedisConfig() { - registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class); - - RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); - RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory", - RedisConnectionFactory.class); - assertThat(repository).isNotNull(); - assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); - assertThat(redisOperations).isNotNull(); - assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) - .isEqualTo(redisConnectionFactory); - } - - @Test - void multipleConnectionFactoryRedisConfig() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> registerAndRefresh(RedisConfig.class, MultipleConnectionFactoryRedisConfig.class)) - .withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class).havingRootCause() - .withMessageContaining("expected single matching bean but found 2"); - } - - @Test - void customIndexResolverConfiguration() { - registerAndRefresh(RedisConfig.class, CustomIndexResolverConfiguration.class); - RedisIndexedSessionRepository repository = this.context.getBean(RedisIndexedSessionRepository.class); - @SuppressWarnings("unchecked") - IndexResolver indexResolver = this.context.getBean(IndexResolver.class); - assertThat(repository).isNotNull(); - assertThat(indexResolver).isNotNull(); - assertThat(repository).hasFieldOrPropertyWithValue("indexResolver", indexResolver); - } - - @Test // gh-1252 - void customRedisMessageListenerContainerConfig() { - registerAndRefresh(RedisConfig.class, CustomRedisMessageListenerContainerConfig.class); - Map beans = this.context - .getBeansOfType(RedisMessageListenerContainer.class); - assertThat(beans).hasSize(2); - assertThat(beans).containsKeys("springSessionRedisMessageListenerContainer", "redisMessageListenerContainer"); - } - - @Test - void sessionRepositoryCustomizer() { - registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class); - RedisIndexedSessionRepository sessionRepository = this.context.getBean(RedisIndexedSessionRepository.class); - assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", - MAX_INACTIVE_INTERVAL_IN_SECONDS); - } - - private void registerAndRefresh(Class... annotatedClasses) { - this.context.register(annotatedClasses); - this.context.refresh(); - } - - private static RedisConnectionFactory mockRedisConnectionFactory() { - RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class); - RedisConnection connectionMock = mock(RedisConnection.class); - RedisServerCommands commandsMock = mock(RedisServerCommands.class); - given(connectionFactoryMock.getConnection()).willReturn(connectionMock); - given(connectionMock.serverCommands()).willReturn(commandsMock); - - Properties keyspaceEventsConfig = new Properties(); - keyspaceEventsConfig.put("notify-keyspace-events", "KEA"); - given(commandsMock.getConfig("notify-keyspace-events")).willReturn(keyspaceEventsConfig); - - willAnswer((it) -> { - SubscriptionListener listener = it.getArgument(0); - listener.onPatternSubscribed(it.getArgument(1), 0); - listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0); - listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0); - - return null; - }).given(connectionMock).pSubscribe(any(), any()); - - return connectionFactoryMock; - } - - @Configuration - static class PropertySourceConfiguration { - - @Bean - PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - - } - - @Configuration - static class RedisConfig { - - @Bean - RedisConnectionFactory defaultRedisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - } - - @Configuration - static class CustomFlushImmediatelySetConfiguration extends RedisIndexedHttpSessionConfiguration { - - CustomFlushImmediatelySetConfiguration() { - setFlushMode(FlushMode.IMMEDIATE); - } - - } - - @Configuration - @EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE, enableIndexingAndEvents = true) - static class CustomFlushImmediatelyConfiguration { - - } - - @Configuration - static class CustomCleanupCronExpressionSetterConfiguration extends RedisIndexedHttpSessionConfiguration { - - CustomCleanupCronExpressionSetterConfiguration() { - setCleanupCron(CLEANUP_CRON_EXPRESSION); - } - - } - - @EnableRedisHttpSession(saveMode = SaveMode.ALWAYS, enableIndexingAndEvents = true) - static class CustomSaveModeExpressionAnnotationConfiguration { - - } - - @Configuration - static class CustomSaveModeExpressionSetterConfiguration extends RedisIndexedHttpSessionConfiguration { - - CustomSaveModeExpressionSetterConfiguration() { - setSaveMode(SaveMode.ALWAYS); - } - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class QualifiedConnectionFactoryRedisConfig { - - @Bean - @SpringSessionRedisConnectionFactory - RedisConnectionFactory qualifiedRedisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class PrimaryConnectionFactoryRedisConfig { - - @Bean - @Primary - RedisConnectionFactory primaryRedisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class QualifiedAndPrimaryConnectionFactoryRedisConfig { - - @Bean - @SpringSessionRedisConnectionFactory - RedisConnectionFactory qualifiedRedisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - @Bean - @Primary - RedisConnectionFactory primaryRedisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class NamedConnectionFactoryRedisConfig { - - @Bean - RedisConnectionFactory redisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class MultipleConnectionFactoryRedisConfig { - - @Bean - RedisConnectionFactory secondaryRedisConnectionFactory() { - return mockRedisConnectionFactory(); - } - - } - - @Configuration - @EnableRedisHttpSession(redisNamespace = "myRedisNamespace", enableIndexingAndEvents = true) - static class CustomRedisHttpSessionConfiguration { - - } - - @Configuration - @EnableRedisHttpSession(redisNamespace = "${session.redis.namespace}", enableIndexingAndEvents = true) - static class CustomRedisHttpSessionConfiguration2 { - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class CustomIndexResolverConfiguration { - - @Bean - @SuppressWarnings("unchecked") - IndexResolver indexResolver() { - return mock(IndexResolver.class); - } - - } - - @Configuration - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class CustomRedisMessageListenerContainerConfig { - - @Bean - RedisMessageListenerContainer redisMessageListenerContainer() { - return mock(RedisMessageListenerContainer.class); - } - - } - - @EnableRedisHttpSession(enableIndexingAndEvents = true) - static class SessionRepositoryCustomizerConfiguration { - - @Bean - @Order(0) - SessionRepositoryCustomizer sessionRepositoryCustomizerOne() { - return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(0); - } - - @Bean - @Order(1) - SessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { - return (sessionRepository) -> sessionRepository - .setDefaultMaxInactiveInterval(MAX_INACTIVE_INTERVAL_IN_SECONDS); - } - - } - -} diff --git a/spring-session-docs/modules/ROOT/examples/resources/docs/http/HttpSessionListenerXmlTests-context.xml b/spring-session-docs/modules/ROOT/examples/resources/docs/http/HttpSessionListenerXmlTests-context.xml index c0f0fdf4a..aafcaa6e7 100644 --- a/spring-session-docs/modules/ROOT/examples/resources/docs/http/HttpSessionListenerXmlTests-context.xml +++ b/spring-session-docs/modules/ROOT/examples/resources/docs/http/HttpSessionListenerXmlTests-context.xml @@ -14,7 +14,7 @@ - + From e1757d2b889053e774f8b99d416df335ebb2d6ae Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Tue, 16 Aug 2022 19:54:51 +0200 Subject: [PATCH 2/2] Restructure Redis HttpSession configuration support This commit restructures configuration support for Redis-backed HttpSession with aim to enable users to easily select the SessionRepository implementation they prefer to use. This is achieved by introducing [at]EnableRedisIndexedHttpSession annotation that can be used to configure RedisIndexedSessionRepository, while the existing [at]EnableRedisHttpSession will going forward configure RedisSessionRepository as the SessionRepository implementation used by Spring Session. Additionally, this also introduces AbstractRedisHttpSessionConfiguration as the base configuration class that manages common aspects of Redis-backed HttpSession support, which is then extended by more specific configuration classes that provide specific SessionRepository implementation. Closes gh-2122 --- .../RedisIndexedSessionRepositoryITests.java | 4 +- .../redis/RedisSessionRepositoryITests.java | 15 +- ...tpSessionExpireSessionDestroyedTests.java} | 4 +- ...isListenerContainerTaskExecutorITests.java | 4 +- .../redis/RedisIndexedSessionRepository.java | 22 +- .../redis/RedisSessionExpirationPolicy.java | 8 +- ...AbstractRedisHttpSessionConfiguration.java | 159 ++++++++ .../web/http/EnableRedisHttpSession.java | 15 +- .../http/EnableRedisIndexedHttpSession.java | 113 ++++++ .../http/RedisHttpSessionConfiguration.java | 311 ++------------- .../RedisIndexedHttpSessionConfiguration.java | 271 +++++++++++++ .../RedisIndexedSessionRepositoryTests.java | 76 ++-- .../RedisSessionExpirationPolicyTests.java | 8 +- ...KeyspaceNotificationsInitializerTests.java | 4 +- .../RedisHttpsSessionConfigurationTests.java | 363 ++++++++++++++++++ ...xedHttpSessionConfigurationMockTests.java} | 4 +- ...urationNoOpConfigureRedisActionTests.java} | 4 +- ...igurationOverrideSessionTaskExecutor.java} | 4 +- ...gurationOverrideSessionTaskExecutors.java} | 4 +- ...IndexedHttpSessionConfigurationTests.java} | 101 ++--- .../annotation/web/http/gh109/Gh109Tests.java | 2 +- ...PathXmlApplicationContextTests-context.xml | 2 +- ...figurationXmlCustomExpireTests-context.xml | 2 +- ...tpSessionConfigurationXmlTests-context.xml | 2 +- .../examples/java/docs/IndexDocTests.java | 4 +- ...OpConfigureRedisActionXmlTests-context.xml | 2 +- .../HttpSessionListenerXmlTests-context.xml | 2 +- spring-session-docs/modules/ROOT/nav.adoc | 1 - .../modules/ROOT/pages/samples.adoc | 4 - ...ng-session-sample-boot-redis-simple.gradle | 22 -- .../java/sample/BootTests.java | 101 ----- .../java/sample/pages/BasePage.java | 38 -- .../java/sample/pages/HomePage.java | 60 --- .../java/sample/pages/LoginPage.java | 66 ---- .../resources/testcontainers.properties | 1 - .../src/main/java/sample/Application.java | 29 -- .../src/main/java/sample/IndexController.java | 35 -- .../java/sample/UserControllerAdvise.java | 37 -- .../java/sample/config/SecurityConfig.java | 43 --- .../java/sample/config/SessionConfig.java | 74 ---- .../src/main/resources/application.properties | 1 - .../src/main/resources/static/favicon.ico | Bin 1150 -> 0 bytes .../src/main/resources/static/images/logo.png | Bin 1123 -> 0 bytes .../src/main/resources/templates/index.html | 11 - .../src/main/resources/templates/layout.html | 127 ------ 45 files changed, 1055 insertions(+), 1105 deletions(-) rename spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/{EnableRedisHttpSessionExpireSessionDestroyedTests.java => EnableRedisIndexedHttpSessionExpireSessionDestroyedTests.java} (95%) create mode 100644 spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/AbstractRedisHttpSessionConfiguration.java create mode 100644 spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java create mode 100644 spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java create mode 100644 spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpsSessionConfigurationTests.java rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisHttpSessionConfigurationMockTests.java => RedisIndexedHttpSessionConfigurationMockTests.java} (95%) rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisHttpSessionConfigurationNoOpConfigureRedisActionTests.java => RedisIndexedHttpSessionConfigurationNoOpConfigureRedisActionTests.java} (96%) rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java => RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java} (97%) rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java => RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java} (97%) rename spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/{RedisHttpSessionConfigurationTests.java => RedisIndexedHttpSessionConfigurationTests.java} (82%) delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/spring-session-sample-boot-redis-simple.gradle delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/BootTests.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/BasePage.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/HomePage.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/LoginPage.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/resources/testcontainers.properties delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/Application.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/IndexController.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/UserControllerAdvise.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SecurityConfig.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SessionConfig.java delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/application.properties delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/static/favicon.ico delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/static/images/logo.png delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/templates/index.html delete mode 100644 spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/templates/layout.html diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java index 243104fdc..fe17d5132 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java @@ -39,7 +39,7 @@ import org.springframework.session.data.SessionEventRegistry; import org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations; -import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession; import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionDestroyedEvent; import org.springframework.test.context.ContextConfiguration; @@ -691,7 +691,7 @@ private String getChangedSecurityName() { } @Configuration - @EnableRedisHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests") + @EnableRedisIndexedHttpSession(redisNamespace = "RedisIndexedSessionRepositoryITests") static class Config extends BaseConfig { @Bean diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java index afe396965..6ea5963a2 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisSessionRepositoryITests.java @@ -26,13 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.session.MapSession; -import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.data.redis.RedisSessionRepository.RedisSession; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; @@ -223,17 +220,9 @@ private static void updateSession(RedisSession session, Instant lastAccessedTime } @Configuration - @EnableSpringHttpSession + @EnableRedisHttpSession static class Config extends BaseConfig { - @Bean - RedisSessionRepository sessionRepository(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory); - redisTemplate.afterPropertiesSet(); - return new RedisSessionRepository(redisTemplate); - } - } } diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSessionExpireSessionDestroyedTests.java similarity index 95% rename from spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java rename to spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSessionExpireSessionDestroyedTests.java index bc91c15fc..744ba6d78 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSessionExpireSessionDestroyedTests.java @@ -44,7 +44,7 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class EnableRedisHttpSessionExpireSessionDestroyedTests extends AbstractRedisITests { +class EnableRedisIndexedHttpSessionExpireSessionDestroyedTests extends AbstractRedisITests { @Autowired private SessionRepository repository; @@ -113,7 +113,7 @@ void setLock(Object lock) { } @Configuration - @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1) + @EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 1) static class Config extends BaseConfig { @Bean diff --git a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java index 8176d2a16..0345e123f 100644 --- a/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java +++ b/spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/taskexecutor/RedisListenerContainerTaskExecutorITests.java @@ -32,7 +32,7 @@ import org.springframework.data.redis.core.RedisOperations; import org.springframework.session.data.redis.AbstractRedisITests; import org.springframework.session.data.redis.config.annotation.SpringSessionRedisOperations; -import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; @@ -101,7 +101,7 @@ boolean taskDispatched() throws InterruptedException { } @Configuration - @EnableRedisHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests") + @EnableRedisIndexedHttpSession(redisNamespace = "RedisListenerContainerTaskExecutorITests") static class Config extends BaseConfig { @Bean diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java index 19f3e0a25..60e82ed04 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisIndexedSessionRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 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. @@ -288,7 +288,7 @@ public class RedisIndexedSessionRepository private byte[] expiredKeyPrefixBytes; - private final RedisOperations sessionRedisOperations; + private final RedisOperations sessionRedisOperations; private final RedisSessionExpirationPolicy expirationPolicy; @@ -314,7 +314,7 @@ public class RedisIndexedSessionRepository * @param sessionRedisOperations the {@link RedisOperations} to use for managing the * sessions. Cannot be null. */ - public RedisIndexedSessionRepository(RedisOperations sessionRedisOperations) { + public RedisIndexedSessionRepository(RedisOperations sessionRedisOperations) { Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null"); this.sessionRedisOperations = sessionRedisOperations; this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this::getExpirationsKey, @@ -406,7 +406,7 @@ private void configureSessionChannels() { * Returns the {@link RedisOperations} used for sessions. * @return the {@link RedisOperations} used for sessions */ - public RedisOperations getSessionRedisOperations() { + public RedisOperations getSessionRedisOperations() { return this.sessionRedisOperations; } @@ -454,7 +454,7 @@ public Map findByIndexNameAndIndexValue(String indexName, * @return the Redis session */ private RedisSession getSession(String id, boolean allowExpired) { - Map entries = getSessionBoundHashOperations(id).entries(); + Map entries = getSessionBoundHashOperations(id).entries(); if (entries.isEmpty()) { return null; } @@ -467,10 +467,10 @@ private RedisSession getSession(String id, boolean allowExpired) { return result; } - private MapSession loadSession(String id, Map entries) { + private MapSession loadSession(String id, Map entries) { MapSession loaded = new MapSession(id); - for (Map.Entry entry : entries.entrySet()) { - String key = (String) entry.getKey(); + for (Map.Entry entry : entries.entrySet()) { + String key = entry.getKey(); if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) { loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue())); } @@ -522,7 +522,7 @@ public void onMessage(Message message, byte[] pattern) { if (ByteUtils.startsWith(messageChannel, this.sessionCreatedChannelPrefixBytes)) { // TODO: is this thread safe? @SuppressWarnings("unchecked") - Map loaded = (Map) this.defaultSerializer.deserialize(message.getBody()); + Map loaded = (Map) this.defaultSerializer.deserialize(message.getBody()); handleCreated(loaded, new String(messageChannel)); return; } @@ -571,7 +571,7 @@ private void cleanupPrincipalIndex(RedisSession session) { } } - private void handleCreated(Map loaded, String channel) { + private void handleCreated(Map loaded, String channel) { String id = channel.substring(channel.lastIndexOf(":") + 1); Session session = loadSession(id, loaded); publishEvent(new SessionCreatedEvent(this, session)); @@ -661,7 +661,7 @@ public String getSessionExpiredChannel() { * @param sessionId the id of the {@link Session} to work with * @return the {@link BoundHashOperations} to operate on a {@link Session} */ - private BoundHashOperations getSessionBoundHashOperations(String sessionId) { + private BoundHashOperations getSessionBoundHashOperations(String sessionId) { String key = getSessionKey(sessionId); return this.sessionRedisOperations.boundHashOps(key); } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java index d67fbc83a..33097813d 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 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. @@ -52,13 +52,13 @@ final class RedisSessionExpirationPolicy { private static final String SESSION_EXPIRES_PREFIX = "expires:"; - private final RedisOperations redis; + private final RedisOperations redis; private final Function lookupExpirationKey; private final Function lookupSessionKey; - RedisSessionExpirationPolicy(RedisOperations sessionRedisOperations, + RedisSessionExpirationPolicy(RedisOperations sessionRedisOperations, Function lookupExpirationKey, Function lookupSessionKey) { super(); this.redis = sessionRedisOperations; @@ -96,7 +96,7 @@ void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) { } String expireKey = getExpirationKey(toExpire); - BoundSetOperations expireOperations = this.redis.boundSetOps(expireKey); + BoundSetOperations expireOperations = this.redis.boundSetOps(expireKey); expireOperations.add(keyToExpire); long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5); diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/AbstractRedisHttpSessionConfiguration.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/AbstractRedisHttpSessionConfiguration.java new file mode 100644 index 000000000..34a953c3e --- /dev/null +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/AbstractRedisHttpSessionConfiguration.java @@ -0,0 +1,159 @@ +/* + * Copyright 2014-2022 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 + * + * https://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.session.data.redis.config.annotation.web.http; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.session.FlushMode; +import org.springframework.session.MapSession; +import org.springframework.session.SaveMode; +import org.springframework.session.Session; +import org.springframework.session.SessionRepository; +import org.springframework.session.config.SessionRepositoryCustomizer; +import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; +import org.springframework.session.data.redis.RedisSessionRepository; +import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; +import org.springframework.util.Assert; + +/** + * Base configuration class for Redis based {@link SessionRepository} implementations. + * + * @param the {@link SessionRepository} type + * @author Vedran Pavic + * @since 3.0.0 + * @see RedisHttpSessionConfiguration + * @see RedisIndexedHttpSessionConfiguration + * @see SpringSessionRedisConnectionFactory + */ +@Configuration(proxyBeanMethods = false) +public abstract class AbstractRedisHttpSessionConfiguration> + extends SpringHttpSessionConfiguration implements BeanClassLoaderAware { + + private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; + + private String redisNamespace = RedisSessionRepository.DEFAULT_KEY_NAMESPACE; + + private FlushMode flushMode = FlushMode.ON_SAVE; + + private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; + + private RedisConnectionFactory redisConnectionFactory; + + private RedisSerializer defaultRedisSerializer; + + private List> sessionRepositoryCustomizers; + + private ClassLoader classLoader; + + public abstract T sessionRepository(); + + public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) { + this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds; + } + + protected Integer getMaxInactiveIntervalInSeconds() { + return this.maxInactiveIntervalInSeconds; + } + + public void setRedisNamespace(String namespace) { + Assert.hasText(namespace, "namespace must not be empty"); + this.redisNamespace = namespace; + } + + protected String getRedisNamespace() { + return this.redisNamespace; + } + + public void setFlushMode(FlushMode flushMode) { + Assert.notNull(flushMode, "flushMode must not be null"); + this.flushMode = flushMode; + } + + protected FlushMode getFlushMode() { + return this.flushMode; + } + + public void setSaveMode(SaveMode saveMode) { + Assert.notNull(saveMode, "saveMode must not be null"); + this.saveMode = saveMode; + } + + protected SaveMode getSaveMode() { + return this.saveMode; + } + + @Autowired + public void setRedisConnectionFactory( + @SpringSessionRedisConnectionFactory ObjectProvider springSessionRedisConnectionFactory, + ObjectProvider redisConnectionFactory) { + this.redisConnectionFactory = springSessionRedisConnectionFactory + .getIfAvailable(redisConnectionFactory::getObject); + } + + protected RedisConnectionFactory getRedisConnectionFactory() { + return this.redisConnectionFactory; + } + + @Autowired(required = false) + @Qualifier("springSessionDefaultRedisSerializer") + public void setDefaultRedisSerializer(RedisSerializer defaultRedisSerializer) { + this.defaultRedisSerializer = defaultRedisSerializer; + } + + protected RedisSerializer getDefaultRedisSerializer() { + return this.defaultRedisSerializer; + } + + @Autowired(required = false) + public void setSessionRepositoryCustomizer( + ObjectProvider> sessionRepositoryCustomizers) { + this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList()); + } + + protected List> getSessionRepositoryCustomizers() { + return this.sessionRepositoryCustomizers; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + protected RedisTemplate createRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + if (getDefaultRedisSerializer() != null) { + redisTemplate.setDefaultSerializer(getDefaultRedisSerializer()); + } + redisTemplate.setConnectionFactory(getRedisConnectionFactory()); + redisTemplate.setBeanClassLoader(this.classLoader); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + +} diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java index 125ab8aa1..df59fc61b 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java @@ -31,14 +31,14 @@ import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; -import org.springframework.session.data.redis.RedisIndexedSessionRepository; +import org.springframework.session.data.redis.RedisSessionRepository; import org.springframework.session.web.http.SessionRepositoryFilter; /** * Add this annotation to an {@code @Configuration} class to expose the * {@link SessionRepositoryFilter} as a bean named {@code springSessionRepositoryFilter} - * and backed by Redis. In order to leverage the annotation, a single - * {@link RedisConnectionFactory} must be provided. For example: + * and backed by {@link RedisSessionRepository}. In order to leverage the annotation, a + * single {@link RedisConnectionFactory} must be provided. For example: * *
  * @Configuration
@@ -84,7 +84,7 @@
 	 * the applications and they could function within the same Redis instance.
 	 * @return the unique namespace for keys
 	 */
-	String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;
+	String redisNamespace() default RedisSessionRepository.DEFAULT_KEY_NAMESPACE;
 
 	/**
 	 * Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only
@@ -98,13 +98,6 @@
 	 */
 	FlushMode flushMode() default FlushMode.ON_SAVE;
 
-	/**
-	 * The cron expression for expired session cleanup job. By default runs every minute.
-	 * @return the session cleanup cron expression
-	 * @since 2.0.0
-	 */
-	String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
-
 	/**
 	 * Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
 	 * only saves changes made to session.
diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java
new file mode 100644
index 000000000..f483fe338
--- /dev/null
+++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisIndexedHttpSession.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2014-2022 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
+ *
+ *      https://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.session.data.redis.config.annotation.web.http;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.session.FlushMode;
+import org.springframework.session.MapSession;
+import org.springframework.session.SaveMode;
+import org.springframework.session.Session;
+import org.springframework.session.SessionRepository;
+import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
+import org.springframework.session.data.redis.RedisIndexedSessionRepository;
+import org.springframework.session.web.http.SessionRepositoryFilter;
+
+/**
+ * Add this annotation to an {@code @Configuration} class to expose the
+ * {@link SessionRepositoryFilter} as a bean named {@code springSessionRepositoryFilter}
+ * and backed by {@link RedisIndexedSessionRepository}. In order to leverage the
+ * annotation, a single {@link RedisConnectionFactory} must be provided. For example:
+ *
+ * 
+ * @Configuration
+ * @EnableRedisIndexedHttpSession
+ * public class RedisHttpSessionConfig {
+ *
+ *     @Bean
+ *     public LettuceConnectionFactory redisConnectionFactory() {
+ *         return new LettuceConnectionFactory();
+ *     }
+ *
+ * }
+ * 
+ * + * More advanced configurations can extend {@link RedisIndexedHttpSessionConfiguration} + * instead. + * + * @author Vedran Pavic + * @since 3.0.0 + * @see EnableSpringHttpSession + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import(RedisIndexedHttpSessionConfiguration.class) +@Configuration(proxyBeanMethods = false) +public @interface EnableRedisIndexedHttpSession { + + /** + * The session timeout in seconds. By default, it is set to 1800 seconds (30 minutes). + * This should be a non-negative integer. + * @return the seconds a session can be inactive before expiring + */ + int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; + + /** + * Defines a unique namespace for keys. The value is used to isolate sessions by + * changing the prefix from default {@code spring:session:} to + * {@code :}. + *

+ * For example, if you had an application named "Application A" that needed to keep + * the sessions isolated from "Application B" you could set two different values for + * the applications and they could function within the same Redis instance. + * @return the unique namespace for keys + */ + String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE; + + /** + * Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only + * updates the backing Redis when {@link SessionRepository#save(Session)} is invoked. + * In a web environment this happens just before the HTTP response is committed. + *

+ * Setting the value to {@code IMMEDIATE} will ensure that the any updates to the + * Session are immediately written to the Redis instance. + * @return the {@link FlushMode} to use + */ + FlushMode flushMode() default FlushMode.ON_SAVE; + + /** + * Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which + * only saves changes made to session. + * @return the save mode + */ + SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE; + + /** + * The cron expression for expired session cleanup job. By default runs every minute. + * @return the session cleanup cron expression + */ + String cleanupCron() default RedisIndexedHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; + +} diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java index 49dec20c6..443095c87 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java @@ -16,61 +16,26 @@ package org.springframework.session.data.redis.config.annotation.web.http; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.time.Duration; import java.util.Map; -import java.util.concurrent.Executor; -import java.util.stream.Collectors; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.listener.ChannelTopic; -import org.springframework.data.redis.listener.PatternTopic; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.session.FlushMode; -import org.springframework.session.IndexResolver; -import org.springframework.session.MapSession; -import org.springframework.session.SaveMode; -import org.springframework.session.Session; -import org.springframework.session.config.SessionRepositoryCustomizer; -import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; -import org.springframework.session.data.redis.RedisIndexedSessionRepository; -import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction; -import org.springframework.session.data.redis.config.ConfigureRedisAction; -import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; +import org.springframework.session.data.redis.RedisSessionRepository; import org.springframework.session.web.http.SessionRepositoryFilter; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; /** * Exposes the {@link SessionRepositoryFilter} as a bean named - * {@code springSessionRepositoryFilter}. In order to use this a single - * {@link RedisConnectionFactory} must be exposed as a Bean. + * {@code springSessionRepositoryFilter} backed by {@link RedisSessionRepository}. In + * order to use this a single {@link RedisConnectionFactory} must be exposed as a Bean. * * @author Rob Winch * @author EddĂș MelĂ©ndez @@ -79,170 +44,27 @@ * @see EnableRedisHttpSession */ @Configuration(proxyBeanMethods = false) -public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration - implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware { - - static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; - - private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; - - private String redisNamespace = RedisIndexedSessionRepository.DEFAULT_NAMESPACE; - - private FlushMode flushMode = FlushMode.ON_SAVE; - - private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; - - private String cleanupCron = DEFAULT_CLEANUP_CRON; - - private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); - - private RedisConnectionFactory redisConnectionFactory; - - private IndexResolver indexResolver; - - private RedisSerializer defaultRedisSerializer; - - private ApplicationEventPublisher applicationEventPublisher; - - private Executor redisTaskExecutor; - - private Executor redisSubscriptionExecutor; - - private List> sessionRepositoryCustomizers; - - private ClassLoader classLoader; +public class RedisHttpSessionConfiguration extends AbstractRedisHttpSessionConfiguration + implements EmbeddedValueResolverAware, ImportAware { private StringValueResolver embeddedValueResolver; @Bean - public RedisIndexedSessionRepository sessionRepository() { - RedisTemplate redisTemplate = createRedisTemplate(); - RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate); - sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); - if (this.indexResolver != null) { - sessionRepository.setIndexResolver(this.indexResolver); - } - if (this.defaultRedisSerializer != null) { - sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); - } - sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); - if (StringUtils.hasText(this.redisNamespace)) { - sessionRepository.setRedisKeyNamespace(this.redisNamespace); + @Override + public RedisSessionRepository sessionRepository() { + RedisTemplate redisTemplate = createRedisTemplate(); + RedisSessionRepository sessionRepository = new RedisSessionRepository(redisTemplate); + sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(getMaxInactiveIntervalInSeconds())); + if (StringUtils.hasText(getRedisNamespace())) { + sessionRepository.setRedisKeyNamespace(getRedisNamespace()); } - sessionRepository.setFlushMode(this.flushMode); - sessionRepository.setSaveMode(this.saveMode); - int database = resolveDatabase(); - sessionRepository.setDatabase(database); - this.sessionRepositoryCustomizers + sessionRepository.setFlushMode(getFlushMode()); + sessionRepository.setSaveMode(getSaveMode()); + getSessionRepositoryCustomizers() .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository)); return sessionRepository; } - @Bean - public RedisMessageListenerContainer springSessionRedisMessageListenerContainer( - RedisIndexedSessionRepository sessionRepository) { - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(this.redisConnectionFactory); - if (this.redisTaskExecutor != null) { - container.setTaskExecutor(this.redisTaskExecutor); - } - if (this.redisSubscriptionExecutor != null) { - container.setSubscriptionExecutor(this.redisSubscriptionExecutor); - } - container.addMessageListener(sessionRepository, - Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()), - new ChannelTopic(sessionRepository.getSessionExpiredChannel()))); - container.addMessageListener(sessionRepository, - Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*"))); - return container; - } - - @Bean - public InitializingBean enableRedisKeyspaceNotificationsInitializer() { - return new EnableRedisKeyspaceNotificationsInitializer(this.redisConnectionFactory, this.configureRedisAction); - } - - public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) { - this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds; - } - - public void setRedisNamespace(String namespace) { - this.redisNamespace = namespace; - } - - public void setFlushMode(FlushMode flushMode) { - Assert.notNull(flushMode, "flushMode cannot be null"); - this.flushMode = flushMode; - } - - public void setSaveMode(SaveMode saveMode) { - this.saveMode = saveMode; - } - - public void setCleanupCron(String cleanupCron) { - this.cleanupCron = cleanupCron; - } - - /** - * Sets the action to perform for configuring Redis. - * @param configureRedisAction the configureRedis to set. The default is - * {@link ConfigureNotifyKeyspaceEventsAction}. - */ - @Autowired(required = false) - public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) { - this.configureRedisAction = configureRedisAction; - } - - @Autowired - public void setRedisConnectionFactory( - @SpringSessionRedisConnectionFactory ObjectProvider springSessionRedisConnectionFactory, - ObjectProvider redisConnectionFactory) { - RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable(); - if (redisConnectionFactoryToUse == null) { - redisConnectionFactoryToUse = redisConnectionFactory.getObject(); - } - this.redisConnectionFactory = redisConnectionFactoryToUse; - } - - @Autowired(required = false) - @Qualifier("springSessionDefaultRedisSerializer") - public void setDefaultRedisSerializer(RedisSerializer defaultRedisSerializer) { - this.defaultRedisSerializer = defaultRedisSerializer; - } - - @Autowired - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - @Autowired(required = false) - public void setIndexResolver(IndexResolver indexResolver) { - this.indexResolver = indexResolver; - } - - @Autowired(required = false) - @Qualifier("springSessionRedisTaskExecutor") - public void setRedisTaskExecutor(Executor redisTaskExecutor) { - this.redisTaskExecutor = redisTaskExecutor; - } - - @Autowired(required = false) - @Qualifier("springSessionRedisSubscriptionExecutor") - public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) { - this.redisSubscriptionExecutor = redisSubscriptionExecutor; - } - - @Autowired(required = false) - public void setSessionRepositoryCustomizer( - ObjectProvider> sessionRepositoryCustomizers) { - this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList()); - } - - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.classLoader = classLoader; - } - @Override public void setEmbeddedValueResolver(StringValueResolver resolver) { this.embeddedValueResolver = resolver; @@ -253,103 +75,16 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { Map attributeMap = importMetadata .getAnnotationAttributes(EnableRedisHttpSession.class.getName()); AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap); - this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds"); + if (attributes == null) { + return; + } + setMaxInactiveIntervalInSeconds(attributes.getNumber("maxInactiveIntervalInSeconds")); String redisNamespaceValue = attributes.getString("redisNamespace"); if (StringUtils.hasText(redisNamespaceValue)) { - this.redisNamespace = this.embeddedValueResolver.resolveStringValue(redisNamespaceValue); + setRedisNamespace(this.embeddedValueResolver.resolveStringValue(redisNamespaceValue)); } - this.flushMode = attributes.getEnum("flushMode"); - this.saveMode = attributes.getEnum("saveMode"); - String cleanupCron = attributes.getString("cleanupCron"); - if (StringUtils.hasText(cleanupCron)) { - this.cleanupCron = cleanupCron; - } - } - - private RedisTemplate createRedisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - if (this.defaultRedisSerializer != null) { - redisTemplate.setDefaultSerializer(this.defaultRedisSerializer); - } - redisTemplate.setConnectionFactory(this.redisConnectionFactory); - redisTemplate.setBeanClassLoader(this.classLoader); - redisTemplate.afterPropertiesSet(); - return redisTemplate; - } - - private int resolveDatabase() { - if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null) - && this.redisConnectionFactory instanceof LettuceConnectionFactory) { - return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase(); - } - if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null) - && this.redisConnectionFactory instanceof JedisConnectionFactory) { - return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase(); - } - return RedisIndexedSessionRepository.DEFAULT_DATABASE; - } - - /** - * Ensures that Redis is configured to send keyspace notifications. This is important - * to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents. - * Without the SessionDestroyedEvent resources may not get cleaned up properly. For - * example, the mapping of the Session to WebSocket connections may not get cleaned - * up. - */ - static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean { - - private final RedisConnectionFactory connectionFactory; - - private ConfigureRedisAction configure; - - EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory, - ConfigureRedisAction configure) { - this.connectionFactory = connectionFactory; - this.configure = configure; - } - - @Override - public void afterPropertiesSet() { - if (this.configure == ConfigureRedisAction.NO_OP) { - return; - } - RedisConnection connection = this.connectionFactory.getConnection(); - try { - this.configure.configure(connection); - } - finally { - try { - connection.close(); - } - catch (Exception ex) { - LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex); - } - } - } - - } - - /** - * Configuration of scheduled job for cleaning up expired sessions. - */ - @EnableScheduling - @Configuration(proxyBeanMethods = false) - class SessionCleanupConfiguration implements SchedulingConfigurer { - - private final RedisIndexedSessionRepository sessionRepository; - - SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) { - this.sessionRepository = sessionRepository; - } - - @Override - public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { - taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions, - RedisHttpSessionConfiguration.this.cleanupCron); - } - + setFlushMode(attributes.getEnum("flushMode")); + setSaveMode(attributes.getEnum("saveMode")); } } diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java new file mode 100644 index 000000000..1635c17ad --- /dev/null +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfiguration.java @@ -0,0 +1,271 @@ +/* + * Copyright 2014-2022 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 + * + * https://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.session.data.redis.config.annotation.web.http; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Executor; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.session.IndexResolver; +import org.springframework.session.Session; +import org.springframework.session.data.redis.RedisIndexedSessionRepository; +import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction; +import org.springframework.session.data.redis.config.ConfigureRedisAction; +import org.springframework.session.web.http.SessionRepositoryFilter; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + * Exposes the {@link SessionRepositoryFilter} as a bean named + * {@code springSessionRepositoryFilter} backed by {@link RedisIndexedSessionRepository}. + * In order to use this a single {@link RedisConnectionFactory} must be exposed as a Bean. + * + * @author Vedran Pavic + * @since 3.0.0 + * @see EnableRedisIndexedHttpSession + */ +@Configuration(proxyBeanMethods = false) +public class RedisIndexedHttpSessionConfiguration + extends AbstractRedisHttpSessionConfiguration + implements EmbeddedValueResolverAware, ImportAware { + + static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; + + private String cleanupCron = DEFAULT_CLEANUP_CRON; + + private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); + + private IndexResolver indexResolver; + + private ApplicationEventPublisher applicationEventPublisher; + + private Executor redisTaskExecutor; + + private Executor redisSubscriptionExecutor; + + private StringValueResolver embeddedValueResolver; + + @Bean + @Override + public RedisIndexedSessionRepository sessionRepository() { + RedisTemplate redisTemplate = createRedisTemplate(); + RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate); + sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); + if (this.indexResolver != null) { + sessionRepository.setIndexResolver(this.indexResolver); + } + if (getDefaultRedisSerializer() != null) { + sessionRepository.setDefaultSerializer(getDefaultRedisSerializer()); + } + sessionRepository.setDefaultMaxInactiveInterval(getMaxInactiveIntervalInSeconds()); + if (StringUtils.hasText(getRedisNamespace())) { + sessionRepository.setRedisKeyNamespace(getRedisNamespace()); + } + sessionRepository.setFlushMode(getFlushMode()); + sessionRepository.setSaveMode(getSaveMode()); + int database = resolveDatabase(); + sessionRepository.setDatabase(database); + getSessionRepositoryCustomizers() + .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository)); + return sessionRepository; + } + + @Bean + public RedisMessageListenerContainer springSessionRedisMessageListenerContainer( + RedisIndexedSessionRepository sessionRepository) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(getRedisConnectionFactory()); + if (this.redisTaskExecutor != null) { + container.setTaskExecutor(this.redisTaskExecutor); + } + if (this.redisSubscriptionExecutor != null) { + container.setSubscriptionExecutor(this.redisSubscriptionExecutor); + } + container.addMessageListener(sessionRepository, + Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()), + new ChannelTopic(sessionRepository.getSessionExpiredChannel()))); + container.addMessageListener(sessionRepository, + Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*"))); + return container; + } + + @Bean + public InitializingBean enableRedisKeyspaceNotificationsInitializer() { + return new EnableRedisKeyspaceNotificationsInitializer(getRedisConnectionFactory(), this.configureRedisAction); + } + + public void setCleanupCron(String cleanupCron) { + this.cleanupCron = cleanupCron; + } + + /** + * Sets the action to perform for configuring Redis. + * @param configureRedisAction the configureRedis to set. The default is + * {@link ConfigureNotifyKeyspaceEventsAction}. + */ + @Autowired(required = false) + public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) { + this.configureRedisAction = configureRedisAction; + } + + @Autowired + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Autowired(required = false) + public void setIndexResolver(IndexResolver indexResolver) { + this.indexResolver = indexResolver; + } + + @Autowired(required = false) + @Qualifier("springSessionRedisTaskExecutor") + public void setRedisTaskExecutor(Executor redisTaskExecutor) { + this.redisTaskExecutor = redisTaskExecutor; + } + + @Autowired(required = false) + @Qualifier("springSessionRedisSubscriptionExecutor") + public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) { + this.redisSubscriptionExecutor = redisSubscriptionExecutor; + } + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map attributeMap = importMetadata + .getAnnotationAttributes(EnableRedisIndexedHttpSession.class.getName()); + AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap); + if (attributes == null) { + return; + } + setMaxInactiveIntervalInSeconds(attributes.getNumber("maxInactiveIntervalInSeconds")); + String redisNamespaceValue = attributes.getString("redisNamespace"); + if (StringUtils.hasText(redisNamespaceValue)) { + setRedisNamespace(this.embeddedValueResolver.resolveStringValue(redisNamespaceValue)); + } + setFlushMode(attributes.getEnum("flushMode")); + setSaveMode(attributes.getEnum("saveMode")); + String cleanupCron = attributes.getString("cleanupCron"); + if (StringUtils.hasText(cleanupCron)) { + setCleanupCron(cleanupCron); + } + } + + private int resolveDatabase() { + if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null) + && getRedisConnectionFactory() instanceof LettuceConnectionFactory) { + return ((LettuceConnectionFactory) getRedisConnectionFactory()).getDatabase(); + } + if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null) + && getRedisConnectionFactory() instanceof JedisConnectionFactory) { + return ((JedisConnectionFactory) getRedisConnectionFactory()).getDatabase(); + } + return RedisIndexedSessionRepository.DEFAULT_DATABASE; + } + + /** + * Ensures that Redis is configured to send keyspace notifications. This is important + * to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents. + * Without the SessionDestroyedEvent resources may not get cleaned up properly. For + * example, the mapping of the Session to WebSocket connections may not get cleaned + * up. + */ + static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean { + + private final RedisConnectionFactory connectionFactory; + + private final ConfigureRedisAction configure; + + EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory, + ConfigureRedisAction configure) { + this.connectionFactory = connectionFactory; + this.configure = configure; + } + + @Override + public void afterPropertiesSet() { + if (this.configure == ConfigureRedisAction.NO_OP) { + return; + } + RedisConnection connection = this.connectionFactory.getConnection(); + try { + this.configure.configure(connection); + } + finally { + try { + connection.close(); + } + catch (Exception ex) { + LogFactory.getLog(getClass()).error("Error closing RedisConnection", ex); + } + } + } + + } + + /** + * Configuration of scheduled job for cleaning up expired sessions. + */ + @EnableScheduling + @Configuration(proxyBeanMethods = false) + class SessionCleanupConfiguration implements SchedulingConfigurer { + + private final RedisIndexedSessionRepository sessionRepository; + + SessionCleanupConfiguration(RedisIndexedSessionRepository sessionRepository) { + this.sessionRepository = sessionRepository; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions, + RedisIndexedHttpSessionConfiguration.this.cleanupCron); + } + + } + +} diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java index 07cd09d5f..5b6c22fa2 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryTests.java @@ -70,16 +70,16 @@ class RedisIndexedSessionRepositoryTests { @Mock - private RedisOperations redisOperations; + private RedisOperations redisOperations; @Mock - private BoundValueOperations boundValueOperations; + private BoundValueOperations boundValueOperations; @Mock - private BoundHashOperations boundHashOperations; + private BoundHashOperations boundHashOperations; @Mock - private BoundSetOperations boundSetOperations; + private BoundSetOperations boundSetOperations; @Mock private ApplicationEventPublisher publisher; @@ -116,7 +116,7 @@ void setApplicationEventPublisherNull() { @Test void changeSessionIdWhenNotSaved() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -133,7 +133,7 @@ void changeSessionIdWhenNotSaved() { @Test void changeSessionIdWhenSaved() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -166,7 +166,7 @@ void createSessionCustomMaxInactiveInterval() { @Test void saveNewSession() { RedisSession session = this.redisRepository.createSession(); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -201,7 +201,7 @@ void saveJavadocSummary() { .roundUpToNextMinute(RedisSessionExpirationPolicy.expiresInMillis(session)); String destroyedTriggerKey = "spring:session:sessions:expires:" + session.getId(); - given(this.redisOperations.boundHashOps(sessionKey)).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(sessionKey)).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(backgroundExpireKey)).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(destroyedTriggerKey)).willReturn(this.boundValueOperations); @@ -224,7 +224,7 @@ void saveJavadoc() { RedisSession session = this.redisRepository.new RedisSession(this.cached, false); session.setLastAccessedTime(session.getLastAccessedTime()); - given(this.redisOperations.boundHashOps("spring:session:sessions:session-id")) + given(this.redisOperations.boundHashOps("spring:session:sessions:session-id")) .willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps("spring:session:expirations:1404361860000")) .willReturn(this.boundSetOperations); @@ -245,7 +245,7 @@ void saveJavadoc() { void saveLastAccessChanged() { RedisSession session = this.redisRepository.new RedisSession(this.cached, false); session.setLastAccessedTime(Instant.ofEpochMilli(12345678L)); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -260,7 +260,7 @@ void saveSetAttribute() { String attrName = "attrName"; RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false); session.setAttribute(attrName, "attrValue"); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -275,7 +275,7 @@ void saveRemoveAttribute() { String attrName = "attrName"; RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false); session.removeAttribute(attrName); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -288,7 +288,7 @@ void saveRemoveAttribute() { void saveExpired() { RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false); session.setMaxInactiveInterval(Duration.ZERO); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); this.redisRepository.save(session); @@ -316,7 +316,7 @@ void delete() { MapSession expected = new MapSession(); expected.setLastAccessedTime(Instant.now().minusSeconds(60)); expected.setAttribute(attrName, "attrValue"); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attrName), expected.getAttribute(attrName), RedisSessionMapper.CREATION_TIME_KEY, expected.getCreationTime().toEpochMilli(), @@ -335,7 +335,7 @@ void delete() { @Test void deleteNullSession() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); String id = "abc"; this.redisRepository.deleteById(id); @@ -347,7 +347,7 @@ void deleteNullSession() { @SuppressWarnings("unchecked") void getSessionNotFound() { String id = "abc"; - given(this.redisOperations.boundHashOps(getKey(id))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(id))).willReturn(this.boundHashOperations); given(this.boundHashOperations.entries()).willReturn(map()); assertThat(this.redisRepository.findById(id)).isNull(); @@ -362,7 +362,8 @@ void getSessionFound() { expected.setLastAccessedTime(Instant.now().minusSeconds(60)); expected.setAttribute(attribute1, "test"); expected.setAttribute(attribute2, null); - given(this.redisOperations.boundHashOps(getKey(expected.getId()))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(expected.getId()))) + .willReturn(this.boundHashOperations); Map map = map(RedisIndexedSessionRepository.getSessionAttrNameKey(attribute1), expected.getAttribute(attribute1), RedisIndexedSessionRepository.getSessionAttrNameKey(attribute2), expected.getAttribute(attribute2), RedisSessionMapper.CREATION_TIME_KEY, @@ -387,7 +388,8 @@ void getSessionFound() { @SuppressWarnings("unchecked") void getSessionExpired() { String expiredId = "expired-id"; - given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(expiredId))) + .willReturn(this.boundHashOperations); Map map = map(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 1, RedisSessionMapper.LAST_ACCESSED_TIME_KEY, Instant.now().minus(5, ChronoUnit.MINUTES).toEpochMilli()); given(this.boundHashOperations.entries()).willReturn(map); @@ -401,7 +403,8 @@ void findByPrincipalNameExpired() { String expiredId = "expired-id"; given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.boundSetOperations.members()).willReturn(Collections.singleton(expiredId)); - given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(expiredId))) + .willReturn(this.boundHashOperations); Map map = map(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 1, RedisSessionMapper.LAST_ACCESSED_TIME_KEY, Instant.now().minus(5, ChronoUnit.MINUTES).toEpochMilli()); given(this.boundHashOperations.entries()).willReturn(map); @@ -420,7 +423,8 @@ void findByPrincipalName() { String sessionId = "some-id"; given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.boundSetOperations.members()).willReturn(Collections.singleton(sessionId)); - given(this.redisOperations.boundHashOps(getKey(sessionId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(sessionId))) + .willReturn(this.boundHashOperations); Map map = map(RedisSessionMapper.CREATION_TIME_KEY, createdTime.toEpochMilli(), RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) maxInactive.getSeconds(), RedisSessionMapper.LAST_ACCESSED_TIME_KEY, lastAccessed.toEpochMilli()); @@ -495,7 +499,8 @@ void onMessageCreatedCustomSerializer() { @SuppressWarnings("unchecked") void onMessageDeletedSessionFound() { String deletedId = "deleted-id"; - given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(deletedId))) + .willReturn(this.boundHashOperations); Map map = map(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 0, RedisSessionMapper.LAST_ACCESSED_TIME_KEY, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5)); given(this.boundHashOperations.entries()).willReturn(map); @@ -522,7 +527,8 @@ void onMessageDeletedSessionFound() { @SuppressWarnings("unchecked") void onMessageDeletedSessionNotFound() { String deletedId = "deleted-id"; - given(this.redisOperations.boundHashOps(getKey(deletedId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(deletedId))) + .willReturn(this.boundHashOperations); given(this.boundHashOperations.entries()).willReturn(map()); String channel = "__keyevent@0__:del"; @@ -545,7 +551,8 @@ void onMessageDeletedSessionNotFound() { @SuppressWarnings("unchecked") void onMessageExpiredSessionFound() { String expiredId = "expired-id"; - given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(expiredId))) + .willReturn(this.boundHashOperations); Map map = map(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, 1, RedisSessionMapper.LAST_ACCESSED_TIME_KEY, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5)); given(this.boundHashOperations.entries()).willReturn(map); @@ -572,7 +579,8 @@ void onMessageExpiredSessionFound() { @SuppressWarnings("unchecked") void onMessageExpiredSessionNotFound() { String expiredId = "expired-id"; - given(this.redisOperations.boundHashOps(getKey(expiredId))).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(getKey(expiredId))) + .willReturn(this.boundHashOperations); given(this.boundHashOperations.entries()).willReturn(map()); String channel = "__keyevent@0__:expired"; @@ -632,7 +640,7 @@ void flushModeOnSaveSetMaxInactiveIntervalInSeconds() { @Test void flushModeImmediateCreate() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -651,7 +659,7 @@ void flushModeImmediateCreate() { @Test // gh-1409 void flushModeImmediateCreateWithCustomMaxInactiveInterval() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); this.redisRepository.setDefaultMaxInactiveInterval(60); @@ -664,7 +672,7 @@ void flushModeImmediateCreateWithCustomMaxInactiveInterval() { @Test void flushModeImmediateSetAttribute() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -681,7 +689,7 @@ void flushModeImmediateSetAttribute() { @Test void flushModeImmediateRemoveAttribute() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -699,7 +707,7 @@ void flushModeImmediateRemoveAttribute() { @Test @SuppressWarnings("unchecked") void flushModeSetMaxInactiveIntervalInSeconds() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -715,7 +723,7 @@ void flushModeSetMaxInactiveIntervalInSeconds() { @Test void flushModeSetLastAccessedTime() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); @@ -742,7 +750,7 @@ void changeRedisNamespace() { this.redisRepository.setRedisKeyNamespace(namespace); RedisSession session = this.redisRepository.new RedisSession(new MapSession(), false); session.setMaxInactiveInterval(Duration.ZERO); - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); this.redisRepository.save(session); @@ -832,7 +840,7 @@ void onMessageExpiredInOtherDatabase() { @Test void saveWithSaveModeOnSetAttribute() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); this.redisRepository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE); @@ -849,7 +857,7 @@ void saveWithSaveModeOnSetAttribute() { @Test void saveWithSaveModeOnGetAttribute() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); this.redisRepository.setSaveMode(SaveMode.ON_GET_ATTRIBUTE); @@ -866,7 +874,7 @@ void saveWithSaveModeOnGetAttribute() { @Test void saveWithSaveModeAlways() { - given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); + given(this.redisOperations.boundHashOps(anyString())).willReturn(this.boundHashOperations); given(this.redisOperations.boundSetOps(anyString())).willReturn(this.boundSetOperations); given(this.redisOperations.boundValueOps(anyString())).willReturn(this.boundValueOperations); this.redisRepository.setSaveMode(SaveMode.ALWAYS); diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java index c9d57c7e7..af4719684 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java @@ -50,16 +50,16 @@ class RedisSessionExpirationPolicyTests { private static final Long ONE_MINUTE_AGO = 1429111652346L; @Mock(lenient = true) - RedisOperations sessionRedisOperations; + RedisOperations sessionRedisOperations; @Mock - BoundSetOperations setOperations; + BoundSetOperations setOperations; @Mock - BoundHashOperations hashOperations; + BoundHashOperations hashOperations; @Mock - BoundValueOperations valueOperations; + BoundValueOperations valueOperations; private RedisSessionExpirationPolicy policy; diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java index 80c59f85c..9729400b4 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisKeyspaceNotificationsInitializerTests.java @@ -55,14 +55,14 @@ class EnableRedisKeyspaceNotificationsInitializerTests { @Captor ArgumentCaptor options; - private RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer; + private RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer initializer; @BeforeEach void setup() { given(this.connectionFactory.getConnection()).willReturn(this.connection); given(this.connection.serverCommands()).willReturn(this.commands); - this.initializer = new RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer( + this.initializer = new RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer( this.connectionFactory, new ConfigureNotifyKeyspaceEventsAction()); } diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpsSessionConfigurationTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpsSessionConfigurationTests.java new file mode 100644 index 000000000..1eca91b0c --- /dev/null +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpsSessionConfigurationTests.java @@ -0,0 +1,363 @@ +/* + * Copyright 2014-2022 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 + * + * https://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.session.data.redis.config.annotation.web.http; + +import java.time.Duration; +import java.util.Properties; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.connection.SubscriptionListener; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.session.FlushMode; +import org.springframework.session.SaveMode; +import org.springframework.session.config.SessionRepositoryCustomizer; +import org.springframework.session.data.redis.RedisSessionRepository; +import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RedisHttpSessionConfiguration}. + * + * @author EddĂș MelĂ©ndez + * @author Mark Paluch + * @author Vedran Pavic + */ +class RedisHttpsSessionConfigurationTests { + + private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600; + + private AnnotationConfigApplicationContext context; + + @BeforeEach + void before() { + this.context = new AnnotationConfigApplicationContext(); + } + + @AfterEach + void after() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + void resolveValue() { + registerAndRefresh(RedisConfig.class, CustomRedisHttpSessionConfiguration.class); + RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("myRedisNamespace"); + } + + @Test + void resolveValueByPlaceholder() { + this.context + .setEnvironment(new MockEnvironment().withProperty("session.redis.namespace", "customRedisNamespace")); + registerAndRefresh(RedisConfig.class, PropertySourceConfiguration.class, + CustomRedisHttpSessionConfiguration2.class); + RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("customRedisNamespace"); + } + + @Test + void customFlushImmediately() { + registerAndRefresh(RedisConfig.class, CustomFlushImmediatelyConfiguration.class); + RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class); + assertThat(sessionRepository).isNotNull(); + assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); + } + + @Test + void setCustomFlushImmediately() { + registerAndRefresh(RedisConfig.class, CustomFlushImmediatelySetConfiguration.class); + RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class); + assertThat(sessionRepository).isNotNull(); + assertThat(ReflectionTestUtils.getField(sessionRepository, "flushMode")).isEqualTo(FlushMode.IMMEDIATE); + } + + @Test + void customSaveModeAnnotation() { + registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionAnnotationConfiguration.class); + assertThat(this.context.getBean(RedisSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", + SaveMode.ALWAYS); + } + + @Test + void qualifiedConnectionFactoryRedisConfig() { + registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class); + + RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory", + RedisConnectionFactory.class); + assertThat(repository).isNotNull(); + assertThat(redisConnectionFactory).isNotNull(); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); + assertThat(redisOperations).isNotNull(); + assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) + .isEqualTo(redisConnectionFactory); + } + + @Test + void primaryConnectionFactoryRedisConfig() { + registerAndRefresh(RedisConfig.class, PrimaryConnectionFactoryRedisConfig.class); + + RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisConnectionFactory redisConnectionFactory = this.context.getBean("primaryRedisConnectionFactory", + RedisConnectionFactory.class); + assertThat(repository).isNotNull(); + assertThat(redisConnectionFactory).isNotNull(); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); + assertThat(redisOperations).isNotNull(); + assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) + .isEqualTo(redisConnectionFactory); + } + + @Test + void qualifiedAndPrimaryConnectionFactoryRedisConfig() { + registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class); + + RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisConnectionFactory redisConnectionFactory = this.context.getBean("qualifiedRedisConnectionFactory", + RedisConnectionFactory.class); + assertThat(repository).isNotNull(); + assertThat(redisConnectionFactory).isNotNull(); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); + assertThat(redisOperations).isNotNull(); + assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) + .isEqualTo(redisConnectionFactory); + } + + @Test + void namedConnectionFactoryRedisConfig() { + registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class); + + RedisSessionRepository repository = this.context.getBean(RedisSessionRepository.class); + RedisConnectionFactory redisConnectionFactory = this.context.getBean("redisConnectionFactory", + RedisConnectionFactory.class); + assertThat(repository).isNotNull(); + assertThat(redisConnectionFactory).isNotNull(); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); + assertThat(redisOperations).isNotNull(); + assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) + .isEqualTo(redisConnectionFactory); + } + + @Test + void multipleConnectionFactoryRedisConfig() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> registerAndRefresh(RedisConfig.class, MultipleConnectionFactoryRedisConfig.class)) + .havingRootCause().withMessageContaining("expected single matching bean but found 2"); + } + + @Test + void sessionRepositoryCustomizer() { + registerAndRefresh(RedisConfig.class, SessionRepositoryCustomizerConfiguration.class); + RedisSessionRepository sessionRepository = this.context.getBean(RedisSessionRepository.class); + assertThat(sessionRepository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", + Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + } + + private void registerAndRefresh(Class... annotatedClasses) { + this.context.register(annotatedClasses); + this.context.refresh(); + } + + private static RedisConnectionFactory mockRedisConnectionFactory() { + RedisConnectionFactory connectionFactoryMock = mock(RedisConnectionFactory.class); + RedisConnection connectionMock = mock(RedisConnection.class); + RedisServerCommands commandsMock = mock(RedisServerCommands.class); + given(connectionFactoryMock.getConnection()).willReturn(connectionMock); + given(connectionMock.serverCommands()).willReturn(commandsMock); + + Properties keyspaceEventsConfig = new Properties(); + keyspaceEventsConfig.put("notify-keyspace-events", "KEA"); + given(commandsMock.getConfig("notify-keyspace-events")).willReturn(keyspaceEventsConfig); + + willAnswer((it) -> { + SubscriptionListener listener = it.getArgument(0); + listener.onPatternSubscribed(it.getArgument(1), 0); + listener.onChannelSubscribed("__keyevent@0__:del".getBytes(), 0); + listener.onChannelSubscribed("__keyevent@0__:expired".getBytes(), 0); + + return null; + }).given(connectionMock).pSubscribe(any(), any()); + + return connectionFactoryMock; + } + + @Configuration + static class PropertySourceConfiguration { + + @Bean + PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + + } + + @Configuration + static class RedisConfig { + + @Bean + RedisConnectionFactory defaultRedisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + } + + @Configuration + static class CustomFlushImmediatelySetConfiguration extends RedisHttpSessionConfiguration { + + CustomFlushImmediatelySetConfiguration() { + setFlushMode(FlushMode.IMMEDIATE); + } + + } + + @Configuration + @EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE) + static class CustomFlushImmediatelyConfiguration { + + } + + @EnableRedisHttpSession(saveMode = SaveMode.ALWAYS) + static class CustomSaveModeExpressionAnnotationConfiguration { + + } + + @Configuration + @EnableRedisHttpSession + static class QualifiedConnectionFactoryRedisConfig { + + @Bean + @SpringSessionRedisConnectionFactory + RedisConnectionFactory qualifiedRedisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + } + + @Configuration + @EnableRedisHttpSession + static class PrimaryConnectionFactoryRedisConfig { + + @Bean + @Primary + RedisConnectionFactory primaryRedisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + } + + @Configuration + @EnableRedisHttpSession + static class QualifiedAndPrimaryConnectionFactoryRedisConfig { + + @Bean + @SpringSessionRedisConnectionFactory + RedisConnectionFactory qualifiedRedisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + @Bean + @Primary + RedisConnectionFactory primaryRedisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + } + + @Configuration + @EnableRedisHttpSession + static class NamedConnectionFactoryRedisConfig { + + @Bean + RedisConnectionFactory redisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + } + + @Configuration + @EnableRedisHttpSession + static class MultipleConnectionFactoryRedisConfig { + + @Bean + RedisConnectionFactory secondaryRedisConnectionFactory() { + return mockRedisConnectionFactory(); + } + + } + + @Configuration + @EnableRedisHttpSession(redisNamespace = "myRedisNamespace") + static class CustomRedisHttpSessionConfiguration { + + } + + @Configuration + @EnableRedisHttpSession(redisNamespace = "${session.redis.namespace}") + static class CustomRedisHttpSessionConfiguration2 { + + } + + @EnableRedisHttpSession + static class SessionRepositoryCustomizerConfiguration { + + @Bean + @Order(0) + SessionRepositoryCustomizer sessionRepositoryCustomizerOne() { + return (sessionRepository) -> sessionRepository.setDefaultMaxInactiveInterval(Duration.ZERO); + } + + @Bean + @Order(1) + SessionRepositoryCustomizer sessionRepositoryCustomizerTwo() { + return (sessionRepository) -> sessionRepository + .setDefaultMaxInactiveInterval(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + } + + } + +} diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationMockTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationMockTests.java similarity index 95% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationMockTests.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationMockTests.java index ab1edae69..7e2ca4df5 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationMockTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationMockTests.java @@ -25,7 +25,7 @@ import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.session.data.redis.config.ConfigureRedisAction; -import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer; +import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import static org.mockito.BDDMockito.given; @@ -35,7 +35,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) -class RedisHttpSessionConfigurationMockTests { +class RedisIndexedHttpSessionConfigurationMockTests { @Mock(lenient = true) RedisConnectionFactory factory; diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationNoOpConfigureRedisActionTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationNoOpConfigureRedisActionTests.java similarity index 96% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationNoOpConfigureRedisActionTests.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationNoOpConfigureRedisActionTests.java index 280978a60..30c9b0d60 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationNoOpConfigureRedisActionTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationNoOpConfigureRedisActionTests.java @@ -40,13 +40,13 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisHttpSessionConfigurationNoOpConfigureRedisActionTests { +class RedisIndexedHttpSessionConfigurationNoOpConfigureRedisActionTests { @Test void redisConnectionFactoryNotUsedSinceNoValidation() { } - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession @Configuration static class Config { diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java similarity index 97% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java index cbd14b7b2..07b727efa 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutor.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor.java @@ -49,7 +49,7 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisHttpSessionConfigurationOverrideSessionTaskExecutor { +class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor { @Autowired RedisMessageListenerContainer redisMessageListenerContainer; @@ -62,8 +62,8 @@ void overrideSessionTaskExecutor() { verify(this.springSessionRedisTaskExecutor, times(1)).execute(any(Runnable.class)); } - @EnableRedisHttpSession @Configuration + @EnableRedisIndexedHttpSession static class Config { @Bean diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java similarity index 97% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java index c79db725b..eebb360d7 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationOverrideSessionTaskExecutors.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors.java @@ -51,7 +51,7 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisHttpSessionConfigurationOverrideSessionTaskExecutors { +class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors { @Autowired RedisMessageListenerContainer redisMessageListenerContainer; @@ -68,8 +68,8 @@ void overrideSessionTaskExecutors() { verify(this.springSessionRedisTaskExecutor, never()).execute(any(Runnable.class)); } - @EnableRedisHttpSession @Configuration + @EnableRedisIndexedHttpSession static class Config { @Bean diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java similarity index 82% rename from spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java rename to spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java index 75fdb1161..fca38d7f2 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationTests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisIndexedHttpSessionConfigurationTests.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,13 +54,11 @@ import static org.mockito.Mockito.mock; /** - * Tests for {@link RedisHttpSessionConfiguration}. + * Tests for {@link RedisIndexedHttpSessionConfiguration}. * - * @author EddĂș MelĂ©ndez - * @author Mark Paluch * @author Vedran Pavic */ -class RedisHttpSessionConfigurationTests { +class RedisIndexedHttpSessionConfigurationTests { private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600; @@ -84,7 +81,8 @@ void after() { @Test void resolveValue() { registerAndRefresh(RedisConfig.class, CustomRedisHttpSessionConfiguration.class); - RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + RedisIndexedHttpSessionConfiguration configuration = this.context + .getBean(RedisIndexedHttpSessionConfiguration.class); assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("myRedisNamespace"); } @@ -94,7 +92,8 @@ void resolveValueByPlaceholder() { .setEnvironment(new MockEnvironment().withProperty("session.redis.namespace", "customRedisNamespace")); registerAndRefresh(RedisConfig.class, PropertySourceConfiguration.class, CustomRedisHttpSessionConfiguration2.class); - RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + RedisIndexedHttpSessionConfiguration configuration = this.context + .getBean(RedisIndexedHttpSessionConfiguration.class); assertThat(ReflectionTestUtils.getField(configuration, "redisNamespace")).isEqualTo("customRedisNamespace"); } @@ -118,16 +117,8 @@ void setCustomFlushImmediately() { void customCleanupCronAnnotation() { registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionAnnotationConfiguration.class); - RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); - assertThat(configuration).isNotNull(); - assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); - } - - @Test - void customCleanupCronSetter() { - registerAndRefresh(RedisConfig.class, CustomCleanupCronExpressionSetterConfiguration.class); - - RedisHttpSessionConfiguration configuration = this.context.getBean(RedisHttpSessionConfiguration.class); + RedisIndexedHttpSessionConfiguration configuration = this.context + .getBean(RedisIndexedHttpSessionConfiguration.class); assertThat(configuration).isNotNull(); assertThat(ReflectionTestUtils.getField(configuration, "cleanupCron")).isEqualTo(CLEANUP_CRON_EXPRESSION); } @@ -139,13 +130,6 @@ void customSaveModeAnnotation() { SaveMode.ALWAYS); } - @Test - void customSaveModeSetter() { - registerAndRefresh(RedisConfig.class, CustomSaveModeExpressionSetterConfiguration.class); - assertThat(this.context.getBean(RedisIndexedSessionRepository.class)).hasFieldOrPropertyWithValue("saveMode", - SaveMode.ALWAYS); - } - @Test void qualifiedConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, QualifiedConnectionFactoryRedisConfig.class); @@ -155,8 +139,9 @@ void qualifiedConnectionFactoryRedisConfig() { RedisConnectionFactory.class); assertThat(repository).isNotNull(); assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); assertThat(redisOperations).isNotNull(); assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) .isEqualTo(redisConnectionFactory); @@ -171,8 +156,9 @@ void primaryConnectionFactoryRedisConfig() { RedisConnectionFactory.class); assertThat(repository).isNotNull(); assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); assertThat(redisOperations).isNotNull(); assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) .isEqualTo(redisConnectionFactory); @@ -187,8 +173,9 @@ void qualifiedAndPrimaryConnectionFactoryRedisConfig() { RedisConnectionFactory.class); assertThat(repository).isNotNull(); assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); assertThat(redisOperations).isNotNull(); assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) .isEqualTo(redisConnectionFactory); @@ -203,8 +190,9 @@ void namedConnectionFactoryRedisConfig() { RedisConnectionFactory.class); assertThat(repository).isNotNull(); assertThat(redisConnectionFactory).isNotNull(); - RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils.getField(repository, - "sessionRedisOperations"); + @SuppressWarnings("unchecked") + RedisOperations redisOperations = (RedisOperations) ReflectionTestUtils + .getField(repository, "sessionRedisOperations"); assertThat(redisOperations).isNotNull(); assertThat(ReflectionTestUtils.getField(redisOperations, "connectionFactory")) .isEqualTo(redisConnectionFactory); @@ -214,8 +202,7 @@ void namedConnectionFactoryRedisConfig() { void multipleConnectionFactoryRedisConfig() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> registerAndRefresh(RedisConfig.class, MultipleConnectionFactoryRedisConfig.class)) - .withCauseInstanceOf(NoUniqueBeanDefinitionException.class).havingCause() - .withMessageContaining("expected single matching bean but found 2"); + .havingRootCause().withMessageContaining("expected single matching bean but found 2"); } @Test @@ -295,7 +282,7 @@ RedisConnectionFactory defaultRedisConnectionFactory() { } @Configuration - static class CustomFlushImmediatelySetConfiguration extends RedisHttpSessionConfiguration { + static class CustomFlushImmediatelySetConfiguration extends RedisIndexedHttpSessionConfiguration { CustomFlushImmediatelySetConfiguration() { setFlushMode(FlushMode.IMMEDIATE); @@ -304,41 +291,23 @@ static class CustomFlushImmediatelySetConfiguration extends RedisHttpSessionConf } @Configuration - @EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE) + @EnableRedisIndexedHttpSession(flushMode = FlushMode.IMMEDIATE) static class CustomFlushImmediatelyConfiguration { } - @EnableRedisHttpSession(cleanupCron = CLEANUP_CRON_EXPRESSION) + @EnableRedisIndexedHttpSession(cleanupCron = CLEANUP_CRON_EXPRESSION) static class CustomCleanupCronExpressionAnnotationConfiguration { } - @Configuration - static class CustomCleanupCronExpressionSetterConfiguration extends RedisHttpSessionConfiguration { - - CustomCleanupCronExpressionSetterConfiguration() { - setCleanupCron(CLEANUP_CRON_EXPRESSION); - } - - } - - @EnableRedisHttpSession(saveMode = SaveMode.ALWAYS) + @EnableRedisIndexedHttpSession(saveMode = SaveMode.ALWAYS) static class CustomSaveModeExpressionAnnotationConfiguration { } @Configuration - static class CustomSaveModeExpressionSetterConfiguration extends RedisHttpSessionConfiguration { - - CustomSaveModeExpressionSetterConfiguration() { - setSaveMode(SaveMode.ALWAYS); - } - - } - - @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class QualifiedConnectionFactoryRedisConfig { @Bean @@ -350,7 +319,7 @@ RedisConnectionFactory qualifiedRedisConnectionFactory() { } @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class PrimaryConnectionFactoryRedisConfig { @Bean @@ -362,7 +331,7 @@ RedisConnectionFactory primaryRedisConnectionFactory() { } @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class QualifiedAndPrimaryConnectionFactoryRedisConfig { @Bean @@ -380,7 +349,7 @@ RedisConnectionFactory primaryRedisConnectionFactory() { } @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class NamedConnectionFactoryRedisConfig { @Bean @@ -391,7 +360,7 @@ RedisConnectionFactory redisConnectionFactory() { } @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class MultipleConnectionFactoryRedisConfig { @Bean @@ -402,19 +371,19 @@ RedisConnectionFactory secondaryRedisConnectionFactory() { } @Configuration - @EnableRedisHttpSession(redisNamespace = "myRedisNamespace") + @EnableRedisIndexedHttpSession(redisNamespace = "myRedisNamespace") static class CustomRedisHttpSessionConfiguration { } @Configuration - @EnableRedisHttpSession(redisNamespace = "${session.redis.namespace}") + @EnableRedisIndexedHttpSession(redisNamespace = "${session.redis.namespace}") static class CustomRedisHttpSessionConfiguration2 { } @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class CustomIndexResolverConfiguration { @Bean @@ -426,7 +395,7 @@ IndexResolver indexResolver() { } @Configuration - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class CustomRedisMessageListenerContainerConfig { @Bean @@ -436,7 +405,7 @@ RedisMessageListenerContainer redisMessageListenerContainer() { } - @EnableRedisHttpSession + @EnableRedisIndexedHttpSession static class SessionRepositoryCustomizerConfiguration { @Bean diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/gh109/Gh109Tests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/gh109/Gh109Tests.java index 26797615e..aba58ee80 100644 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/gh109/Gh109Tests.java +++ b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/gh109/Gh109Tests.java @@ -66,7 +66,7 @@ static class Config extends RedisHttpSessionConfiguration { * override sessionRepository construction to set the custom session-timeout */ @Bean - RedisIndexedSessionRepository sessionRepository(RedisOperations sessionRedisTemplate, + RedisIndexedSessionRepository sessionRepository(RedisOperations sessionRedisTemplate, ApplicationEventPublisher applicationEventPublisher) { RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(sessionRedisTemplate); sessionRepository.setDefaultMaxInactiveInterval(this.sessionTimeout); diff --git a/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationClassPathXmlApplicationContextTests-context.xml b/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationClassPathXmlApplicationContextTests-context.xml index f60708c62..efa78fb6c 100644 --- a/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationClassPathXmlApplicationContextTests-context.xml +++ b/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationClassPathXmlApplicationContextTests-context.xml @@ -7,7 +7,7 @@ - + diff --git a/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationXmlCustomExpireTests-context.xml b/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationXmlCustomExpireTests-context.xml index 8a3d6aa5a..ad70969aa 100644 --- a/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationXmlCustomExpireTests-context.xml +++ b/spring-session-data-redis/src/test/resources/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationXmlCustomExpireTests-context.xml @@ -8,7 +8,7 @@ - - + diff --git a/spring-session-docs/modules/ROOT/examples/java/docs/IndexDocTests.java b/spring-session-docs/modules/ROOT/examples/java/docs/IndexDocTests.java index 1e96061ee..3326c1aea 100644 --- a/spring-session-docs/modules/ROOT/examples/java/docs/IndexDocTests.java +++ b/spring-session-docs/modules/ROOT/examples/java/docs/IndexDocTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2022 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. @@ -130,7 +130,7 @@ void newRedisSessionRepository() { @SuppressWarnings("unused") void newRedisIndexedSessionRepository() { // tag::new-redisindexedsessionrepository[] - RedisTemplate redisTemplate = new RedisTemplate<>(); + RedisTemplate redisTemplate = new RedisTemplate<>(); // ... configure redisTemplate ... diff --git a/spring-session-docs/modules/ROOT/examples/resources/docs/HttpSessionConfigurationNoOpConfigureRedisActionXmlTests-context.xml b/spring-session-docs/modules/ROOT/examples/resources/docs/HttpSessionConfigurationNoOpConfigureRedisActionXmlTests-context.xml index 631a86dd4..654186f63 100644 --- a/spring-session-docs/modules/ROOT/examples/resources/docs/HttpSessionConfigurationNoOpConfigureRedisActionXmlTests-context.xml +++ b/spring-session-docs/modules/ROOT/examples/resources/docs/HttpSessionConfigurationNoOpConfigureRedisActionXmlTests-context.xml @@ -10,7 +10,7 @@ - + - + diff --git a/spring-session-docs/modules/ROOT/nav.adoc b/spring-session-docs/modules/ROOT/nav.adoc index dfb517124..68e0daa61 100644 --- a/spring-session-docs/modules/ROOT/nav.adoc +++ b/spring-session-docs/modules/ROOT/nav.adoc @@ -4,7 +4,6 @@ *** HttpSession **** Redis ***** {gh-samples-url}spring-session-sample-boot-redis-json[JSON serialization] -***** {gh-samples-url}spring-session-sample-boot-redis-simple[Simple Redis] ***** xref:guides/boot-redis.adoc[Redis with Events] **** xref:guides/boot-mongo.adoc[MongoDB] **** xref:guides/boot-jdbc.adoc[JDBC] diff --git a/spring-session-docs/modules/ROOT/pages/samples.adoc b/spring-session-docs/modules/ROOT/pages/samples.adoc index 9fb8f670f..e6e92f51d 100644 --- a/spring-session-docs/modules/ROOT/pages/samples.adoc +++ b/spring-session-docs/modules/ROOT/pages/samples.adoc @@ -39,10 +39,6 @@ To get started with Spring Session, the best place to start is our Sample Applic | Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using JSON serialization. | -| {gh-samples-url}spring-session-sample-boot-redis-simple[HttpSession with simple Redis `SessionRepository`] -| Demonstrates how to use Spring Session to replace the `HttpSession` with Redis using `RedisSessionRepository`. -| - | {gh-samples-url}spring-session-sample-boot-mongodb-traditional[Spring Session with MongoDB Repositories (servlet-based)] | Demonstrates how to back Spring Session with traditional MongoDB repositories. | link:guides/boot-mongo.html[Spring Session with MongoDB Repositories] diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/spring-session-sample-boot-redis-simple.gradle b/spring-session-samples/spring-session-sample-boot-redis-simple/spring-session-sample-boot-redis-simple.gradle deleted file mode 100644 index 701dec3bb..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/spring-session-sample-boot-redis-simple.gradle +++ /dev/null @@ -1,22 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample-boot' - -dependencies { - implementation project(':spring-session-data-redis') - implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-devtools' - implementation 'org.webjars:bootstrap' - implementation 'org.webjars:html5shiv' - implementation 'org.webjars:webjars-locator-core' - - testImplementation 'org.junit.jupiter:junit-jupiter-api' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - - integrationTestCompile "org.seleniumhq.selenium:htmlunit-driver" - integrationTestCompile "org.seleniumhq.selenium:selenium-support" - integrationTestCompile 'org.testcontainers:testcontainers' -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/BootTests.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/BootTests.java deleted file mode 100644 index d7ed614ae..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/BootTests.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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 sample; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.openqa.selenium.WebDriver; -import org.testcontainers.containers.GenericContainer; -import sample.pages.HomePage; -import sample.pages.LoginPage; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder; - -@ExtendWith(SpringExtension.class) -@AutoConfigureMockMvc -@SpringBootTest(webEnvironment = WebEnvironment.MOCK) -class BootTests { - - private static final String DOCKER_IMAGE = "redis:7.0.4-alpine"; - - @Autowired - private MockMvc mockMvc; - - private WebDriver driver; - - @BeforeEach - void setup() { - this.driver = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(this.mockMvc).build(); - } - - @AfterEach - void tearDown() { - this.driver.quit(); - } - - @Test - void home() { - LoginPage login = HomePage.go(this.driver); - login.assertAt(); - } - - @Test - void login() { - LoginPage login = HomePage.go(this.driver); - HomePage home = login.form().login(HomePage.class); - home.assertAt(); - home.containCookie("SESSION"); - home.doesNotContainCookie("JSESSIONID"); - } - - @Test - void logout() { - LoginPage login = HomePage.go(this.driver); - HomePage home = login.form().login(HomePage.class); - home.logout(); - login.assertAt(); - } - - @TestConfiguration - static class Config { - - @Bean - GenericContainer redisContainer() { - GenericContainer redisContainer = new GenericContainer(DOCKER_IMAGE).withExposedPorts(6379); - redisContainer.start(); - return redisContainer; - } - - @Bean - LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisContainer().getHost(), redisContainer().getFirstMappedPort()); - } - - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/BasePage.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/BasePage.java deleted file mode 100644 index 9159c0787..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/BasePage.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2014-2019 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 - * - * https://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 sample.pages; - -import org.openqa.selenium.WebDriver; - -public class BasePage { - - private WebDriver driver; - - public BasePage(WebDriver driver) { - this.driver = driver; - } - - public WebDriver getDriver() { - return this.driver; - } - - public static void get(WebDriver driver, String get) { - String baseUrl = "http://localhost"; - driver.get(baseUrl + get); - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/HomePage.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/HomePage.java deleted file mode 100644 index ab9aa87eb..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/HomePage.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014-2019 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 - * - * https://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 sample.pages; - -import java.util.Set; - -import org.openqa.selenium.By; -import org.openqa.selenium.Cookie; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; - -import static org.assertj.core.api.Assertions.assertThat; - -public class HomePage extends BasePage { - - public HomePage(WebDriver driver) { - super(driver); - } - - public static LoginPage go(WebDriver driver) { - get(driver, "/"); - return PageFactory.initElements(driver, LoginPage.class); - } - - public void assertAt() { - assertThat(getDriver().getTitle()).isEqualTo("Spring Session Sample - Secured Content"); - } - - public void containCookie(String cookieName) { - Set cookies = getDriver().manage().getCookies(); - assertThat(cookies).extracting("name").contains(cookieName); - } - - public void doesNotContainCookie(String cookieName) { - Set cookies = getDriver().manage().getCookies(); - assertThat(cookies).extracting("name").doesNotContain(cookieName); - } - - public HomePage logout() { - WebElement logout = getDriver().findElement(By.cssSelector("input[type=\"submit\"]")); - logout.click(); - return PageFactory.initElements(getDriver(), HomePage.class); - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/LoginPage.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/LoginPage.java deleted file mode 100644 index e72bba1eb..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/java/sample/pages/LoginPage.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2014-2019 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 - * - * https://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 sample.pages; - -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; -import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory; - -import static org.assertj.core.api.Assertions.assertThat; - -public class LoginPage extends BasePage { - - public LoginPage(WebDriver driver) { - super(driver); - } - - public void assertAt() { - assertThat(getDriver().getTitle()).isEqualTo("Please sign in"); - } - - public Form form() { - return new Form(getDriver()); - } - - public class Form { - - @FindBy(name = "username") - private WebElement username; - - @FindBy(name = "password") - private WebElement password; - - @FindBy(tagName = "button") - private WebElement button; - - public Form(SearchContext context) { - PageFactory.initElements(new DefaultElementLocatorFactory(context), this); - } - - public T login(Class page) { - this.username.sendKeys("user"); - this.password.sendKeys("password"); - this.button.click(); - return PageFactory.initElements(getDriver(), page); - } - - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/resources/testcontainers.properties b/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/resources/testcontainers.properties deleted file mode 100644 index e3e834192..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/integration-test/resources/testcontainers.properties +++ /dev/null @@ -1 +0,0 @@ -ryuk.container.timeout=120 diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/Application.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/Application.java deleted file mode 100644 index e9c3db94f..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/Application.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014-2019 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 - * - * https://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 sample; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/IndexController.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/IndexController.java deleted file mode 100644 index 25418e262..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/IndexController.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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 sample; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -/** - * An index controller. - * - * @author Rob Winch - */ -@Controller -public class IndexController { - - @GetMapping("/") - String index() { - return "index"; - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/UserControllerAdvise.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/UserControllerAdvise.java deleted file mode 100644 index c7df27e84..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/UserControllerAdvise.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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 sample; - -import java.security.Principal; - -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ModelAttribute; - -/** - * {@link ControllerAdvice} to expose user related attributes. - * - * @author Rob Winch - */ -@ControllerAdvice -public class UserControllerAdvise { - - @ModelAttribute("currentUserName") - String currentUser(Principal principal) { - return (principal != null) ? principal.getName() : null; - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SecurityConfig.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SecurityConfig.java deleted file mode 100644 index 66e93ea8f..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SecurityConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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 sample.config; - -import org.springframework.boot.autoconfigure.security.servlet.PathRequest; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.web.SecurityFilterChain; - -@Configuration -public class SecurityConfig { - - // @formatter:off - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http - .authorizeRequests((authorize) -> authorize - .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() - .anyRequest().authenticated() - ) - .formLogin((formLogin) -> formLogin - .permitAll() - ) - .build(); - } - // @formatter:on - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SessionConfig.java b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SessionConfig.java deleted file mode 100644 index aa2eb6dce..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/java/sample/config/SessionConfig.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2014-2022 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 - * - * https://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 sample.config; - -import java.time.Duration; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.session.RedisSessionProperties; -import org.springframework.boot.autoconfigure.session.SessionProperties; -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.data.redis.core.RedisOperations; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; -import org.springframework.session.data.redis.RedisSessionRepository; - -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(RedisSessionProperties.class) -@EnableSpringHttpSession -public class SessionConfig { - - private final SessionProperties sessionProperties; - - private final RedisSessionProperties redisSessionProperties; - - private final RedisConnectionFactory redisConnectionFactory; - - public SessionConfig(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, - ObjectProvider redisConnectionFactory) { - this.sessionProperties = sessionProperties; - this.redisSessionProperties = redisSessionProperties; - this.redisConnectionFactory = redisConnectionFactory.getObject(); - } - - @Bean - public RedisOperations sessionRedisOperations() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(this.redisConnectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - return redisTemplate; - } - - @Bean - public RedisSessionRepository sessionRepository(RedisOperations sessionRedisOperations) { - RedisSessionRepository sessionRepository = new RedisSessionRepository(sessionRedisOperations); - Duration timeout = this.sessionProperties.getTimeout(); - if (timeout != null) { - sessionRepository.setDefaultMaxInactiveInterval(timeout); - } - sessionRepository.setRedisKeyNamespace(this.redisSessionProperties.getNamespace()); - sessionRepository.setFlushMode(this.redisSessionProperties.getFlushMode()); - sessionRepository.setSaveMode(this.redisSessionProperties.getSaveMode()); - return sessionRepository; - } - -} diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/application.properties b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/application.properties deleted file mode 100644 index 1b5271b53..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.security.user.password=password diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/static/favicon.ico b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/static/favicon.ico deleted file mode 100644 index bfb9974019d4b8b978cf34ea67f0c6804f9b76c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYI*xFHzK2NLHVUHb1iG2*{Y zgB?lOds5VYkY4wx3IDZQ-O0jkQ{(?@bp=p{C&v8O!h&CXe)}H=o_u_bRSig==8O!i zc=x^YFzx?WUq6CX4M?xX{1U9V`}7vL{^Em6v8n;-RbSqS702#f2-g1(NLsD$!KwzN zS8ZJ%R&20o^8Y{o{{6r8@eNjaxV~+({;Q#3&F!=Q_uf49|L5O7|6hFn@<03RE(|p= zJ*s<_{YS^WcP_&H4q|}(5Orn`hB}ZwmBSk_@QDwvQS^g2lONu|PzTm~V&{JqG(7$3 zJq-P6H_u_H1L;#fa{vQto;~z`$*ZUT-~ayo|MAc7|C=7&#!v&-2UM#}0rtFhmTdiw fS5E!E^7Zq7x2tDJHpBVK>Hp`xeEbh92gs!XBi)`m diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/static/images/logo.png b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/static/images/logo.png deleted file mode 100644 index 393230883fb4e04829339f7c21b6ab6de7393891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1123 zcmV-p1f2VcP)P000yS1^@s74{)X{0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU$1xZ9fRCwBAJakr>0R&#%?*!8<7oReN z=>H6CRSb*_9RHalf3;=rzRt=3!VW|AET)fs_IS z5aXsJDqv;5UUXCgspCNG%qCu4_4m8Q8J4{_Ss=DB)G%-}OK$)A_qiv-M@As~;az|L zf?EIsP`7cheE7}?GEN=HaRK6^jLh%PvWj*xv3z@3xac)oRV-9P3=lIhyklZuIsRdC zEQk#P>`Ve}KmUCIvzcu9Tf*+YVP*hfGu{$8D7y_HfEf3kMztIrynEOQW2*Ct&A;*G zO}I7VlHICc5w;GSdsSFH1nBbCHtgWCZ;JNm@ z`XjO?7RGP?S?;`M26}|$>d$}7+Q^c380tVF%ErLJjAqwYZkA^sS%0xHF}~wxlbiD5 z`(qoR=no*41>&DbDvtqiaJE+C3lIk&fG}bK9l%ly6jZSM`Nu#MU}XG#RzRlJh~>*q zMz|m_S~GwYKiDe_40nN83W%$i`8HWIa~)&&`WqMkpMdet@v+_G7x%2~|M)9$Tk#!Y zFw1)&gOROi`~TlS|Ne#uGBU8e{?G6W7-39r7#RMspu`I>^-8__`jg@PS4KGI{tDI)dzZ%dlS>M59FF!%vUTz)~b4X4$^~`TvW7 ziIMs5zyE)kk;ES}`AIdM1t$C=P~wMS3!!9Ap!k0ap%i9haWBalIY!4O?f(oAd%r+4 zo)k2;d7%b_#Jhnp-2~zQ1Q@}XCIrw@0t|34GX2=V#dp;1>j!5(mhZn98K?y0fw-87 z<#h&!;7V3zwkKdg)?fU+ET4a(DSgvn03;_eFu%5AV7&(n_piW02N=Hpff?mLFf{&g z0%=ZYR2CvOD9l@{`Ma!o3LDu1eZb}An>(a<~Q?=8{fF=xc@W!MoZ)b z{O|{gu_$Ev|5TXe{Z}Tay7jj||2@OO@D(hD)hsRsjrwm4H=1C4P6p+J-x==O;Vpp} zelf7T{l>^3$L4GI;MZmjLQek2%+jdD-YkChTV1vRXZ6WjKj)c!`-fi-2P5B87D!gv za)A+O=YLR%j|aSGxW@31vGu^;|BMVrzfaux>)%&1LJk9(o(#_DK=28Oy@5ho@oC^@ z5m@o{&xh3zgTK{rGV>dM|NF@cm+`=a;1wp - - Secured Content - - -
-

Secured Page

-

This page is secured using Spring Boot, Spring Session, and Spring Security.

-
- - diff --git a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/templates/layout.html b/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/templates/layout.html deleted file mode 100644 index 140c16e00..000000000 --- a/spring-session-samples/spring-session-sample-boot-redis-simple/src/main/resources/templates/layout.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - Spring Session Sample - - - - - - - - - - - -
- - -
-
- Some Success message -
-
- Fake content -
-
- -
-
- - - -