Add support for configuring Redis session cleanup cron
This commit is contained in:
@@ -20,9 +20,3 @@ dependencies {
|
||||
integrationTestCompile seleniumDependencies
|
||||
integrationTestCompile "org.testcontainers:testcontainers"
|
||||
}
|
||||
|
||||
integrationTest {
|
||||
doFirst {
|
||||
systemProperties['spring.session.redis.namespace'] = project.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
* @Configuration
|
||||
* @EnableRedisHttpSession
|
||||
* public class RedisHttpSessionConfig {
|
||||
*
|
||||
* {@literal @Bean}
|
||||
* @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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
spring.session.cleanup.cron.expression=oops
|
||||
Reference in New Issue
Block a user