Add support for configuring Redis session cleanup cron

This commit is contained in:
Vedran Pavic
2017-10-27 13:20:59 +02:00
committed by Rob Winch
parent 9ea1fb9af1
commit 7e26897ec2
7 changed files with 115 additions and 135 deletions

View File

@@ -20,9 +20,3 @@ dependencies {
integrationTestCompile seleniumDependencies
integrationTestCompile "org.testcontainers:testcontainers"
}
integrationTest {
doFirst {
systemProperties['spring.session.redis.namespace'] = project.name
}
}

View File

@@ -39,7 +39,6 @@ import org.springframework.data.redis.serializer.RedisSerializer;
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 class RedisOperationsSessionRepository implements
}
}
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}

View File

@@ -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 @@ import org.springframework.session.data.redis.RedisFlushMode;
* 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: <pre>
* <code>
* {@literal @Configuration}
* {@literal @EnableRedisHttpSession}
* must be provided. For example:
*
* <pre class="code">
* &#064;Configuration
* &#064;EnableRedisHttpSession
* public class RedisHttpSessionConfig {
*
* {@literal @Bean}
* &#064;Bean
* public LettuceConnectionFactory connectionFactory() {
* return new LettuceConnectionFactory();
* }
*
* }
* </code> </pre>
* </pre>
*
* 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 @@ public @interface EnableRedisHttpSession {
* @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;
}

View File

@@ -32,7 +32,6 @@ 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.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.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.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.redis.RedisFlushMode;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
@@ -68,7 +69,9 @@ import org.springframework.util.StringValueResolver;
@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<Object> 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<Object, Object> 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 class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
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 class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
this.redisFlushMode = redisFlushMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
/**
* Sets the action to perform for configuring Redis.
*
@@ -187,6 +187,12 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
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 class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata
Map<String, Object> 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<Object, Object> createRedisTemplate(
@@ -233,13 +249,6 @@ public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguratio
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.

View File

@@ -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;
}
}
}

View File

@@ -52,6 +52,8 @@ import static org.mockito.Mockito.mock;
*/
public class RedisHttpSessionConfigurationTests {
private static final String CLEANUP_CRON_EXPRESSION = "0 0 * * * *";
@Rule
public final ExpectedException thrown = ExpectedException.none();
@@ -90,6 +92,30 @@ public class RedisHttpSessionConfigurationTests {
.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 class RedisHttpSessionConfigurationTests {
}
@Test
public void qualifiedAndPrimaryDataSourceConfiguration() {
public void qualifiedAndPrimaryConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class,
QualifiedAndPrimaryConnectionFactoryRedisConfig.class);
@@ -144,7 +170,7 @@ public class RedisHttpSessionConfigurationTests {
}
@Test
public void namedDataSourceConfiguration() {
public void namedConnectionFactoryRedisConfig() {
registerAndRefresh(RedisConfig.class, NamedConnectionFactoryRedisConfig.class);
RedisOperationsSessionRepository repository = this.context
@@ -161,12 +187,11 @@ public class RedisHttpSessionConfigurationTests {
}
@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 class RedisHttpSessionConfigurationTests {
}
@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 class RedisHttpSessionConfigurationTests {
@Configuration
@EnableRedisHttpSession
static class PrimaryConnectionFactoryRedisConfig extends RedisConfig {
static class PrimaryConnectionFactoryRedisConfig {
@Bean
@Primary
@@ -228,7 +268,7 @@ public class RedisHttpSessionConfigurationTests {
@Configuration
@EnableRedisHttpSession
static class QualifiedAndPrimaryConnectionFactoryRedisConfig extends RedisConfig {
static class QualifiedAndPrimaryConnectionFactoryRedisConfig {
@Bean
@SpringSessionRedisConnectionFactory
@@ -246,7 +286,7 @@ public class RedisHttpSessionConfigurationTests {
@Configuration
@EnableRedisHttpSession
static class NamedConnectionFactoryRedisConfig extends RedisConfig {
static class NamedConnectionFactoryRedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
@@ -257,7 +297,7 @@ public class RedisHttpSessionConfigurationTests {
@Configuration
@EnableRedisHttpSession
static class MultipleConnectionFactoryRedisConfig extends RedisConfig {
static class MultipleConnectionFactoryRedisConfig {
@Bean
public RedisConnectionFactory secondaryRedisConnectionFactory() {

View File

@@ -1 +0,0 @@
spring.session.cleanup.cron.expression=oops