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 501b2d22..243104fd 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 @@ class RedisIndexedSessionRepositoryITests extends AbstractRedisITests { } @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 6ea5963a..afe39696 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.Test; 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 @@ class RedisSessionRepositoryITests extends AbstractRedisITests { } @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 10fd1415..bc91c15f 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 @@ class EnableRedisHttpSessionExpireSessionDestroyedTests exten } @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 e70f159c..8176d2a1 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 @@ class RedisListenerContainerTaskExecutorITests extends AbstractRedisITests { } @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 4df42777..125ab8aa 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.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; /** @@ -54,8 +53,7 @@ import org.springframework.session.web.http.SessionRepositoryFilter; * } * * - * 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 @@ import org.springframework.session.web.http.SessionRepositoryFilter; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented -@Import(RedisHttpSessionConfigurationSelector.class) +@Import(RedisHttpSessionConfiguration.class) @Configuration(proxyBeanMethods = false) public @interface EnableRedisHttpSession { @@ -100,6 +98,13 @@ public @interface EnableRedisHttpSession { */ 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 @@ public @interface EnableRedisHttpSession { */ 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 923a382d..49dec20c 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 @@ import org.springframework.util.StringValueResolver; 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 class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio 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 class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio 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 class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio } 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 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio 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 704e2628..00000000 --- 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 67614d50..00000000 --- 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 9729400b..80c59f85 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 7e2ca4df..ab1edae6 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.mockito.junit.jupiter.MockitoExtension; 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.verify; 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 1c346bd6..cbd14b7b 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 @@ import static org.mockito.Mockito.verify; @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor { +class RedisHttpSessionConfigurationOverrideSessionTaskExecutor { @Autowired RedisMessageListenerContainer redisMessageListenerContainer; @@ -62,7 +62,7 @@ class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutor { 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 50afca01..c79db725 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 @@ import static org.mockito.Mockito.verify; @ExtendWith(SpringExtension.class) @ContextConfiguration @WebAppConfiguration -class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors { +class RedisHttpSessionConfigurationOverrideSessionTaskExecutors { @Autowired RedisMessageListenerContainer redisMessageListenerContainer; @@ -68,7 +68,7 @@ class RedisIndexedHttpSessionConfigurationOverrideSessionTaskExecutors { 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 d2a92404..75fdb116 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.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.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 @@ import static org.mockito.Mockito.mock; */ 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 @@ class RedisHttpSessionConfigurationTests { @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 @@ class RedisHttpSessionConfigurationTests { @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 @@ class RedisHttpSessionConfigurationTests { 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 @@ class RedisHttpSessionConfigurationTests { 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 @@ class RedisHttpSessionConfigurationTests { 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 @@ class RedisHttpSessionConfigurationTests { 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 @@ class RedisHttpSessionConfigurationTests { .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 @@ class RedisHttpSessionConfigurationTests { } + @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 @@ class RedisHttpSessionConfigurationTests { } + @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 7c9833c1..00000000 --- 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 c0f0fdf4..aafcaa6e 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 @@ - +