diff --git a/samples/boot/findbyusername/spring-session-sample-boot-findbyusername.gradle b/samples/boot/findbyusername/spring-session-sample-boot-findbyusername.gradle index d994f177e..8bbcbddfe 100644 --- a/samples/boot/findbyusername/spring-session-sample-boot-findbyusername.gradle +++ b/samples/boot/findbyusername/spring-session-sample-boot-findbyusername.gradle @@ -20,9 +20,3 @@ dependencies { integrationTestCompile seleniumDependencies integrationTestCompile "org.testcontainers:testcontainers" } - -integrationTest { - doFirst { - systemProperties['spring.session.redis.namespace'] = project.name - } -} diff --git a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java index 668961210..f4e5e1559 100644 --- a/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java +++ b/spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java @@ -39,7 +39,6 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; import org.springframework.session.Session; @@ -401,7 +400,6 @@ public void save(RedisSession session) { } } - @Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); } 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 c768f987a..5c14a3de4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ 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; @@ -31,32 +33,40 @@ * Add this annotation to an {@code @Configuration} class to expose the * SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and backed by * Redis. In order to leverage the annotation, a single {@link RedisConnectionFactory} - * must be provided. For example:
- * 
- * {@literal @Configuration}
- * {@literal @EnableRedisHttpSession}
+ * must be provided. For example:
+ *
+ * 
+ * @Configuration
+ * @EnableRedisHttpSession
  * public class RedisHttpSessionConfig {
  *
- *     {@literal @Bean}
+ *     @Bean
  *     public LettuceConnectionFactory connectionFactory() {
  *         return new LettuceConnectionFactory();
  *     }
  *
  * }
- *  
+ *
* * More advanced configurations can extend {@link RedisHttpSessionConfiguration} instead. * * @author Rob Winch + * @author Vedran Pavic * @since 1.0 * @see EnableSpringHttpSession */ -@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) -@Target({ java.lang.annotation.ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) @Documented @Import(RedisHttpSessionConfiguration.class) @Configuration public @interface EnableRedisHttpSession { + + /** + * 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 1800; /** @@ -93,4 +103,11 @@ * @since 1.1 */ RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE; + + /** + * The cron expression for expired session cleanup job. By default runs every minute. + * @return the session cleanup cron expression + */ + String cleanupCron() default RedisHttpSessionConfiguration.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 e180031fe..af7d42ba2 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 @@ -32,7 +32,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.redis.connection.RedisConnection; @@ -43,6 +42,8 @@ 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.config.annotation.web.http.SpringHttpSessionConfiguration; import org.springframework.session.data.redis.RedisFlushMode; import org.springframework.session.data.redis.RedisOperationsSessionRepository; @@ -68,7 +69,9 @@ @Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration - implements EmbeddedValueResolverAware, ImportAware { + implements EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer { + + static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; private Integer maxInactiveIntervalInSeconds = 1800; @@ -76,12 +79,16 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE; + private String cleanupCron = DEFAULT_CLEANUP_CRON; + private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction(); private RedisConnectionFactory redisConnectionFactory; private RedisSerializer defaultRedisSerializer; + private ApplicationEventPublisher applicationEventPublisher; + private Executor redisTaskExecutor; private Executor redisSubscriptionExecutor; @@ -89,21 +96,19 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio private StringValueResolver embeddedValueResolver; @Bean - public RedisOperationsSessionRepository sessionRepository( - ApplicationEventPublisher applicationEventPublisher) { + public RedisOperationsSessionRepository sessionRepository() { RedisTemplate redisTemplate = createRedisTemplate( this.redisConnectionFactory, this.defaultRedisSerializer); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository( redisTemplate); - sessionRepository.setApplicationEventPublisher(applicationEventPublisher); + sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); if (this.defaultRedisSerializer != null) { sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); } sessionRepository .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); - String redisNamespace = getRedisNamespace(); - if (StringUtils.hasText(redisNamespace)) { - sessionRepository.setRedisKeyNamespace(redisNamespace); + if (StringUtils.hasText(this.redisNamespace)) { + sessionRepository.setRedisKeyNamespace(this.redisNamespace); } sessionRepository.setRedisFlushMode(this.redisFlushMode); return sessionRepository; @@ -135,15 +140,6 @@ public InitializingBean enableRedisKeyspaceNotificationsInitializer() { this.redisConnectionFactory, this.configureRedisAction); } - /** - * Property placeholder to process the @Scheduled annotation. - * @return the {@link PropertySourcesPlaceholderConfigurer} to use - */ - @Bean - public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) { this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds; } @@ -157,6 +153,10 @@ public void setRedisFlushMode(RedisFlushMode redisFlushMode) { this.redisFlushMode = redisFlushMode; } + public void setCleanupCron(String cleanupCron) { + this.cleanupCron = cleanupCron; + } + /** * Sets the action to perform for configuring Redis. * @@ -187,6 +187,12 @@ public void setDefaultRedisSerializer( this.defaultRedisSerializer = defaultRedisSerializer; } + @Autowired + public void setApplicationEventPublisher( + ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + @Autowired(required = false) @Qualifier("springSessionRedisTaskExecutor") public void setRedisTaskExecutor(Executor redisTaskExecutor) { @@ -206,17 +212,27 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) { @Override public void setImportMetadata(AnnotationMetadata importMetadata) { - Map enableAttrMap = importMetadata + Map attributeMap = importMetadata .getAnnotationAttributes(EnableRedisHttpSession.class.getName()); - AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap); - this.maxInactiveIntervalInSeconds = enableAttrs + AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap); + this.maxInactiveIntervalInSeconds = attributes .getNumber("maxInactiveIntervalInSeconds"); - String redisNamespaceValue = enableAttrs.getString("redisNamespace"); + String redisNamespaceValue = attributes.getString("redisNamespace"); if (StringUtils.hasText(redisNamespaceValue)) { this.redisNamespace = this.embeddedValueResolver .resolveStringValue(redisNamespaceValue); } - this.redisFlushMode = enableAttrs.getEnum("redisFlushMode"); + this.redisFlushMode = attributes.getEnum("redisFlushMode"); + String cleanupCron = attributes.getString("cleanupCron"); + if (StringUtils.hasText(cleanupCron)) { + this.cleanupCron = cleanupCron; + } + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(), + this.cleanupCron); } private static RedisTemplate createRedisTemplate( @@ -233,13 +249,6 @@ private static RedisTemplate createRedisTemplate( return redisTemplate; } - private String getRedisNamespace() { - if (StringUtils.hasText(this.redisNamespace)) { - return this.redisNamespace; - } - return System.getProperty("spring.session.redis.namespace", ""); - } - /** * Ensures that Redis is configured to send keyspace notifications. This is important * to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents. diff --git a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationCustomCronTests.java b/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationCustomCronTests.java deleted file mode 100644 index bc06d9f5f..000000000 --- a/spring-session-data-redis/src/test/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfigurationCustomCronTests.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.session.data.redis.config.annotation.web.http; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * @author Rob Winch - * - */ -public class RedisHttpSessionConfigurationCustomCronTests { - - AnnotationConfigApplicationContext context; - - @Before - public void setup() { - this.context = new AnnotationConfigApplicationContext(); - } - - @After - public void closeContext() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void overrideCron() { - this.context.register(Config.class); - - assertThatThrownBy(() -> - RedisHttpSessionConfigurationCustomCronTests.this.context.refresh()) - .hasStackTraceContaining( - "Encountered invalid @Scheduled method 'cleanupExpiredSessions': Cron expression must consist of 6 fields (found 1 in \"oops\")"); - } - - @EnableRedisHttpSession - @Configuration - @PropertySource("classpath:spring-session-cleanup-cron-expression-oops.properties") - static class Config { - @Bean - public RedisConnectionFactory connectionFactory() { - RedisConnectionFactory factory = mock(RedisConnectionFactory.class); - RedisConnection connection = mock(RedisConnection.class); - given(factory.getConnection()).willReturn(connection); - - return factory; - } - } -} 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 8b485b72f..da83540fe 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 @@ -52,6 +52,8 @@ */ public class RedisHttpSessionConfigurationTests { + private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *"; + @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -90,6 +92,30 @@ public void resolveValueByPlaceholder() { .isEqualTo("customRedisNamespace"); } + @Test + public 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 + public 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 public void qualifiedConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, @@ -126,7 +152,7 @@ public void primaryConnectionFactoryRedisConfig() { } @Test - public void qualifiedAndPrimaryDataSourceConfiguration() { + public void qualifiedAndPrimaryConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, QualifiedAndPrimaryConnectionFactoryRedisConfig.class); @@ -144,7 +170,7 @@ public void qualifiedAndPrimaryDataSourceConfiguration() { } @Test - public void namedDataSourceConfiguration() { + public void namedConnectionFactoryRedisConfig() { registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class); RedisOperationsSessionRepository repository = this.context @@ -161,12 +187,11 @@ public void namedDataSourceConfiguration() { } @Test - public void multipleDataSourceConfiguration() { + public void multipleConnectionFactoryRedisConfig() { this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage( - "secondaryRedisConnectionFactory,defaultRedisConnectionFactory"); + this.thrown.expectMessage("expected single matching bean but found 2"); - registerAndRefresh(MultipleConnectionFactoryRedisConfig.class); + registerAndRefresh(RedisConfig.class, MultipleConnectionFactoryRedisConfig.class); } private void registerAndRefresh(Class... annotatedClasses) { @@ -202,9 +227,24 @@ public RedisConnectionFactory defaultRedisConnectionFactory() { } + @EnableRedisHttpSession(cleanupCron = CLEANUP_CRON_EXPRESSION) + static class CustomCleanupCronExpressionAnnotationConfiguration { + + } + + @Configuration + static class CustomCleanupCronExpressionSetterConfiguration + extends RedisHttpSessionConfiguration { + + CustomCleanupCronExpressionSetterConfiguration() { + setCleanupCron(CLEANUP_CRON_EXPRESSION); + } + + } + @Configuration @EnableRedisHttpSession - static class QualifiedConnectionFactoryRedisConfig extends RedisConfig { + static class QualifiedConnectionFactoryRedisConfig { @Bean @SpringSessionRedisConnectionFactory @@ -216,7 +256,7 @@ public RedisConnectionFactory qualifiedRedisConnectionFactory() { @Configuration @EnableRedisHttpSession - static class PrimaryConnectionFactoryRedisConfig extends RedisConfig { + static class PrimaryConnectionFactoryRedisConfig { @Bean @Primary @@ -228,7 +268,7 @@ public RedisConnectionFactory primaryRedisConnectionFactory() { @Configuration @EnableRedisHttpSession - static class QualifiedAndPrimaryConnectionFactoryRedisConfig extends RedisConfig { + static class QualifiedAndPrimaryConnectionFactoryRedisConfig { @Bean @SpringSessionRedisConnectionFactory @@ -246,7 +286,7 @@ public RedisConnectionFactory primaryRedisConnectionFactory() { @Configuration @EnableRedisHttpSession - static class NamedConnectionFactoryRedisConfig extends RedisConfig { + static class NamedConnectionFactoryRedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { @@ -257,7 +297,7 @@ public RedisConnectionFactory redisConnectionFactory() { @Configuration @EnableRedisHttpSession - static class MultipleConnectionFactoryRedisConfig extends RedisConfig { + static class MultipleConnectionFactoryRedisConfig { @Bean public RedisConnectionFactory secondaryRedisConnectionFactory() { diff --git a/spring-session-data-redis/src/test/resources/spring-session-cleanup-cron-expression-oops.properties b/spring-session-data-redis/src/test/resources/spring-session-cleanup-cron-expression-oops.properties deleted file mode 100644 index 17db1ef20..000000000 --- a/spring-session-data-redis/src/test/resources/spring-session-cleanup-cron-expression-oops.properties +++ /dev/null @@ -1 +0,0 @@ -spring.session.cleanup.cron.expression=oops \ No newline at end of file