Add property to configure Spring Session Redis repository type

With Spring Session moving to RedisSessionRepository as the preferred
session repository, Spring Boot auto-configuration should make it
possible to easily switch back to the previous default
(RedisIndexedSessionRepository).

This commit introduces spring.session.redis.repository configuration
property that allows selecting the desired Redis-backed session
repository implementation.

See gh-32205
This commit is contained in:
Vedran Pavic 2022-08-30 10:39:00 +02:00 committed by Andy Wilkinson
parent 6175c4210d
commit 3093380e35
4 changed files with 129 additions and 46 deletions

View File

@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -32,6 +33,7 @@ import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction; import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction; import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration; import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration;
/** /**
@ -50,30 +52,62 @@ import org.springframework.session.data.redis.config.annotation.web.http.RedisIn
@EnableConfigurationProperties(RedisSessionProperties.class) @EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration { class RedisSessionConfiguration {
@Bean @Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean @ConditionalOnProperty(prefix = "spring.session.redis", name = "repository", havingValue = "default",
ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) { matchIfMissing = true)
return switch (redisSessionProperties.getConfigureAction()) { static class DefaultRedisSessionConfiguration {
case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsAction();
case NONE -> ConfigureRedisAction.NO_OP; @Configuration(proxyBeanMethods = false)
}; public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}
setRedisNamespace(redisSessionProperties.getNamespace());
setFlushMode(redisSessionProperties.getFlushMode());
setSaveMode(redisSessionProperties.getSaveMode());
}
}
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
public static class SpringBootRedisHttpSessionConfiguration extends RedisIndexedHttpSessionConfiguration { @ConditionalOnProperty(prefix = "spring.session.redis", name = "repository", havingValue = "indexed")
static class IndexedRedisSessionConfiguration {
@Autowired @Bean
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, @ConditionalOnMissingBean
ServerProperties serverProperties) { ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
Duration timeout = sessionProperties return switch (redisSessionProperties.getConfigureAction()) {
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()); case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsAction();
if (timeout != null) { case NONE -> ConfigureRedisAction.NO_OP;
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); };
}
@Configuration(proxyBeanMethods = false)
public static class SpringBootRedisIndexedHttpSessionConfiguration
extends RedisIndexedHttpSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}
setRedisNamespace(redisSessionProperties.getNamespace());
setFlushMode(redisSessionProperties.getFlushMode());
setSaveMode(redisSessionProperties.getSaveMode());
setCleanupCron(redisSessionProperties.getCleanupCron());
} }
setRedisNamespace(redisSessionProperties.getNamespace());
setFlushMode(redisSessionProperties.getFlushMode());
setSaveMode(redisSessionProperties.getSaveMode());
setCleanupCron(redisSessionProperties.getCleanupCron());
} }
} }

View File

@ -2757,6 +2757,20 @@
"name": "spring.session.redis.flush-mode", "name": "spring.session.redis.flush-mode",
"defaultValue": "on-save" "defaultValue": "on-save"
}, },
{
"name": "spring.session.redis.repository",
"description": "Redis session repository implementation to use.",
"values": [
{
"value": "default",
"description": "Use default Redis session repository."
},
{
"value": "indexed",
"description": "Use indexed Redis session repository."
}
]
},
{ {
"name": "spring.session.redis.save-mode", "name": "spring.session.redis.save-mode",
"defaultValue": "on-set-attribute" "defaultValue": "on-set-attribute"
@ -3362,4 +3376,4 @@
] ]
} }
] ]
} }

View File

@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.session.RedisSessionConfiguration.SpringBootRedisHttpSessionConfiguration; import org.springframework.boot.autoconfigure.session.RedisSessionConfiguration.IndexedRedisSessionConfiguration.SpringBootRedisIndexedHttpSessionConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
@ -38,6 +38,7 @@ import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode; import org.springframework.session.SaveMode;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository; import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction; import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction; import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository; import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
@ -70,8 +71,8 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio
.withPropertyValues("spring.data.redis.host=" + redis.getHost(), .withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()) "spring.data.redis.port=" + redis.getFirstMappedPort())
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.run(validateSpringSessionUsesRedis("spring:session:event:0:created:", FlushMode.ON_SAVE, .run(validateSpringSessionUsesDefaultRedis("spring:session:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *")); SaveMode.ON_SET_ATTRIBUTE));
} }
@Test @Test
@ -79,8 +80,8 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.data.redis.host=" + redis.getHost(), .withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()) "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesRedis("spring:session:event:0:created:", FlushMode.ON_SAVE, .run(validateSpringSessionUsesDefaultRedis("spring:session:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *")); SaveMode.ON_SET_ATTRIBUTE));
} }
@Test @Test
@ -89,64 +90,97 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio
.withPropertyValues("spring.data.redis.host=" + redis.getHost(), .withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m") "spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m")
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)).run((context) -> { .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)).run((context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context, RedisSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class); RedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60); assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
Duration.ofMinutes(1));
}); });
} }
@Test @Test
void redisSessionStoreWithCustomizations() { void defaultRedisSessionStoreWithCustomizations() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.session.redis.namespace=foo", "spring.session.redis.flush-mode=immediate", .withPropertyValues("spring.session.redis.namespace=foo", "spring.session.redis.flush-mode=immediate",
"spring.session.redis.save-mode=on-get-attribute", "spring.session.redis.save-mode=on-get-attribute", "spring.data.redis.host=" + redis.getHost(),
"spring.session.redis.cleanup-cron=0 0 12 * * *", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()) "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesRedis("foo:event:0:created:", FlushMode.IMMEDIATE, .run(validateSpringSessionUsesDefaultRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE));
SaveMode.ON_GET_ATTRIBUTE, "0 0 12 * * *"));
} }
@Test @Test
void redisSessionWithConfigureActionNone() { void indexedRedisSessionDefaultConfig() {
this.contextRunner.withPropertyValues("spring.session.redis.repository=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.run(validateSpringSessionUsesIndexedRedis("spring:session:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *"));
}
@Test
void indexedRedisSessionStoreWithCustomizations() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.session.redis.configure-action=none", .withPropertyValues("spring.session.redis.repository=indexed", "spring.session.redis.namespace=foo",
"spring.data.redis.host=" + redis.getHost(), "spring.session.redis.flush-mode=immediate", "spring.session.redis.save-mode=on-get-attribute",
"spring.session.redis.cleanup-cron=0 0 12 * * *", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesIndexedRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE,
"0 0 12 * * *"));
}
@Test
void indexedRedisSessionWithConfigureActionNone() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.session.redis.repository=indexed",
"spring.session.redis.configure-action=none", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()) "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureRedisAction.NO_OP.getClass())); .run(validateStrategy(ConfigureRedisAction.NO_OP.getClass()));
} }
@Test @Test
void redisSessionWithDefaultConfigureActionNone() { void indexedRedisSessionWithDefaultConfigureActionNone() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.data.redis.host=" + redis.getHost(), .withPropertyValues("spring.session.redis.repository=indexed",
"spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()) "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureNotifyKeyspaceEventsAction.class, .run(validateStrategy(ConfigureNotifyKeyspaceEventsAction.class,
entry("notify-keyspace-events", "gxE"))); entry("notify-keyspace-events", "gxE")));
} }
@Test @Test
void redisSessionWithCustomConfigureRedisActionBean() { void indexedRedisSessionWithCustomConfigureRedisActionBean() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withUserConfiguration(MaxEntriesRedisAction.class) .withUserConfiguration(MaxEntriesRedisAction.class)
.withPropertyValues("spring.data.redis.host=" + redis.getHost(), .withPropertyValues("spring.session.redis.repository=indexed",
"spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()) "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(MaxEntriesRedisAction.class, entry("set-max-intset-entries", "1024"))); .run(validateStrategy(MaxEntriesRedisAction.class, entry("set-max-intset-entries", "1024")));
} }
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesRedis( private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesDefaultRedis(String keyNamespace,
String sessionCreatedChannelPrefix, FlushMode flushMode, SaveMode saveMode, String cleanupCron) { FlushMode flushMode, SaveMode saveMode) {
return (context) -> {
RedisSessionRepository repository = validateSessionRepository(context, RedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
new ServerProperties().getServlet().getSession().getTimeout());
assertThat(repository).hasFieldOrPropertyWithValue("keyNamespace", keyNamespace);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
};
}
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesIndexedRedis(String keyNamespace,
FlushMode flushMode, SaveMode saveMode, String cleanupCron) {
return (context) -> { return (context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context, RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class); RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
(int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds()); (int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds());
assertThat(repository.getSessionCreatedChannelPrefix()).isEqualTo(sessionCreatedChannelPrefix); assertThat(repository).hasFieldOrPropertyWithValue("namespace", keyNamespace);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode); assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode);
SpringBootRedisHttpSessionConfiguration configuration = context
.getBean(SpringBootRedisHttpSessionConfiguration.class);
assertThat(configuration).hasFieldOrPropertyWithValue("cleanupCron", cleanupCron);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode); assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
SpringBootRedisIndexedHttpSessionConfiguration configuration = context
.getBean(SpringBootRedisIndexedHttpSessionConfiguration.class);
assertThat(configuration).hasFieldOrPropertyWithValue("cleanupCron", cleanupCron);
}; };
} }

View File

@ -1,3 +1,4 @@
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
spring.security.user.name=user spring.security.user.name=user
spring.security.user.password=password spring.security.user.password=password
spring.session.redis.repository=indexed