Skip to content

Add support for configuring Spring Session Redis repository #32205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand All @@ -32,6 +33,7 @@
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.web.http.RedisHttpSessionConfiguration;
import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration;

/**
Expand All @@ -50,30 +52,62 @@
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {

@Bean
@ConditionalOnMissingBean
ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
return switch (redisSessionProperties.getConfigureAction()) {
case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsAction();
case NONE -> ConfigureRedisAction.NO_OP;
};
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.session.redis", name = "repository", havingValue = "default",
matchIfMissing = true)
static class DefaultRedisSessionConfiguration {

@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)
public static class SpringBootRedisHttpSessionConfiguration 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());
@ConditionalOnProperty(prefix = "spring.session.redis", name = "repository", havingValue = "indexed")
static class IndexedRedisSessionConfiguration {

@Bean
@ConditionalOnMissingBean
ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
return switch (redisSessionProperties.getConfigureAction()) {
case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsAction();
case NONE -> ConfigureRedisAction.NO_OP;
};
}

@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());

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2757,6 +2757,20 @@
"name": "spring.session.redis.flush-mode",
"defaultValue": "on-save"
},
{
"name": "spring.session.redis.repository",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't 100% sure how to name this property, so I initially went with the simple option. Alternatively, this could maybe be named spring.session.redis.repository-type.

"description": "Redis session repository implementation to use.",
"values": [
{
"value": "default",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisSessionRepository by itself doesn't offer much inspiration on how to name this other than default or maybe basic.

"description": "Use default Redis session repository."
},
{
"value": "indexed",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH, RedisIndexedSessionRepository makes this quite straightforward. One might get tempted to throw mention of events here as well (based on soon to be removed EnableRedisHttpSession#enableIndexingAndEvents) however I'd avoid that as support for session events is an implementation detail of a session repository (and not something expressed through a contract, like indexing capabilities).

"description": "Use indexed Redis session repository."
}
]
},
{
"name": "spring.session.redis.save-mode",
"defaultValue": "on-set-attribute"
Expand Down Expand Up @@ -3362,4 +3376,4 @@
]
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import org.springframework.boot.autoconfigure.AutoConfigurations;
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.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
Expand All @@ -38,6 +38,7 @@
import org.springframework.session.SaveMode;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
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.ConfigureRedisAction;
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
Expand Down Expand Up @@ -70,17 +71,17 @@ void defaultConfig() {
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.run(validateSpringSessionUsesRedis("spring:session:event:0:created:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *"));
.run(validateSpringSessionUsesDefaultRedis("spring:session:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE));
}

@Test
void redisTakesPrecedenceMultipleImplementations() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesRedis("spring:session:event:0:created:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *"));
.run(validateSpringSessionUsesDefaultRedis("spring:session:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE));
}

@Test
Expand All @@ -89,64 +90,97 @@ void defaultConfigWithCustomTimeout() {
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m")
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)).run((context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60);
RedisSessionRepository repository = validateSessionRepository(context,
RedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
Duration.ofMinutes(1));
});
}

@Test
void redisSessionStoreWithCustomizations() {
void defaultRedisSessionStoreWithCustomizations() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.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.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesDefaultRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE));
}

@Test
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))
.withPropertyValues("spring.session.redis.repository=indexed", "spring.session.redis.namespace=foo",
"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(validateSpringSessionUsesRedis("foo:event:0:created:", FlushMode.IMMEDIATE,
SaveMode.ON_GET_ATTRIBUTE, "0 0 12 * * *"));
.run(validateSpringSessionUsesIndexedRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE,
"0 0 12 * * *"));
}

@Test
void redisSessionWithConfigureActionNone() {
void indexedRedisSessionWithConfigureActionNone() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.withPropertyValues("spring.session.redis.configure-action=none",
"spring.data.redis.host=" + redis.getHost(),
.withPropertyValues("spring.session.redis.repository=indexed",
"spring.session.redis.configure-action=none", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureRedisAction.NO_OP.getClass()));
}

@Test
void redisSessionWithDefaultConfigureActionNone() {
void indexedRedisSessionWithDefaultConfigureActionNone() {
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())
.run(validateStrategy(ConfigureNotifyKeyspaceEventsAction.class,
entry("notify-keyspace-events", "gxE")));
}

@Test
void redisSessionWithCustomConfigureRedisActionBean() {
void indexedRedisSessionWithCustomConfigureRedisActionBean() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.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())
.run(validateStrategy(MaxEntriesRedisAction.class, entry("set-max-intset-entries", "1024")));

}

private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesRedis(
String sessionCreatedChannelPrefix, FlushMode flushMode, SaveMode saveMode, String cleanupCron) {
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesDefaultRedis(String keyNamespace,
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) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
(int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds());
assertThat(repository.getSessionCreatedChannelPrefix()).isEqualTo(sessionCreatedChannelPrefix);
assertThat(repository).hasFieldOrPropertyWithValue("namespace", keyNamespace);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode);
SpringBootRedisHttpSessionConfiguration configuration = context
.getBean(SpringBootRedisHttpSessionConfiguration.class);
assertThat(configuration).hasFieldOrPropertyWithValue("cleanupCron", cleanupCron);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
SpringBootRedisIndexedHttpSessionConfiguration configuration = context
.getBean(SpringBootRedisIndexedHttpSessionConfiguration.class);
assertThat(configuration).hasFieldOrPropertyWithValue("cleanupCron", cleanupCron);
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
management.endpoints.web.exposure.include=*
spring.security.user.name=user
spring.security.user.password=password
spring.session.redis.repository=indexed