From ea4b53d6aea2648c0186f76bb53a76687df61024 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 22 Oct 2024 11:31:42 +0100 Subject: [PATCH] Add auto-configuration for an indexed reactive session repository Closes gh-42604 --- ...iveSessionAutoConfigurationRedisTests.java | 94 +++++++++++++++++++ .../RedisReactiveSessionConfiguration.java | 69 +++++++++++--- .../session/RedisSessionProperties.java | 16 ++-- 3 files changed, 159 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/dockerTest/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/dockerTest/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java index c582849233..5387e5579e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/dockerTest/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/dockerTest/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java @@ -18,28 +18,38 @@ package org.springframework.boot.autoconfigure.session; import java.time.Duration; import java.util.List; +import java.util.Map; import com.redis.testcontainers.RedisContainer; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.http.ResponseCookie; import org.springframework.session.MapSession; import org.springframework.session.SaveMode; import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; +import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository; import org.springframework.session.data.redis.ReactiveRedisSessionRepository; +import org.springframework.session.data.redis.config.ConfigureReactiveRedisAction; +import org.springframework.session.data.redis.config.annotation.ConfigureNotifyKeyspaceEventsReactiveAction; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Reactive Redis-specific tests for {@link SessionAutoConfiguration}. @@ -121,6 +131,52 @@ class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConf })); } + @Test + void indexedRedisSessionDefaultConfig() { + this.contextRunner + .withPropertyValues("spring.session.redis.repository-type=indexed", + "spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort()) + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) + .run(validateSpringSessionUsesIndexedRedis("spring:session:", SaveMode.ON_SET_ATTRIBUTE)); + } + + @Test + void indexedRedisSessionStoreWithCustomizations() { + this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) + .withPropertyValues("spring.session.redis.repository-type=indexed", "spring.session.redis.namespace=foo", + "spring.session.redis.save-mode=on-get-attribute", "spring.data.redis.host=" + redis.getHost(), + "spring.data.redis.port=" + redis.getFirstMappedPort()) + .run(validateSpringSessionUsesIndexedRedis("foo:", SaveMode.ON_GET_ATTRIBUTE)); + } + + @Test + void indexedRedisSessionWithConfigureActionNone() { + this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) + .withPropertyValues("spring.session.redis.repository-type=indexed", + "spring.session.redis.configure-action=none", "spring.data.redis.host=" + redis.getHost(), + "spring.data.redis.port=" + redis.getFirstMappedPort()) + .run(validateStrategy(ConfigureReactiveRedisAction.NO_OP.getClass())); + } + + @Test + void indexedRedisSessionWithDefaultConfigureActionNone() { + this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) + .withPropertyValues("spring.session.redis.repository-type=indexed", + "spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort()) + .run(validateStrategy(ConfigureNotifyKeyspaceEventsReactiveAction.class, + entry("notify-keyspace-events", "gxE"))); + } + + @Test + void indexedRedisSessionWithCustomConfigureReactiveRedisActionBean() { + this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) + .withUserConfiguration(MaxEntriesReactiveRedisAction.class) + .withPropertyValues("spring.session.redis.repository-type=indexed", + "spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort()) + .run(validateStrategy(MaxEntriesReactiveRedisAction.class, entry("set-max-intset-entries", "1024"))); + + } + private ContextConsumer validateSpringSessionUsesRedis(String namespace, SaveMode saveMode) { return (context) -> { @@ -133,4 +189,42 @@ class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConf }; } + private ContextConsumer validateSpringSessionUsesIndexedRedis( + String keyNamespace, SaveMode saveMode) { + return (context) -> { + ReactiveRedisIndexedSessionRepository repository = validateSessionRepository(context, + ReactiveRedisIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", + new ServerProperties().getReactive().getSession().getTimeout()); + assertThat(repository).hasFieldOrPropertyWithValue("namespace", keyNamespace); + assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode); + }; + } + + private ContextConsumer validateStrategy( + Class expectedConfigureReactiveRedisActionType, + Map.Entry... expectedConfig) { + return (context) -> { + assertThat(context).hasSingleBean(ConfigureReactiveRedisAction.class); + assertThat(context).hasSingleBean(RedisConnectionFactory.class); + assertThat(context.getBean(ConfigureReactiveRedisAction.class)) + .isInstanceOf(expectedConfigureReactiveRedisActionType); + ReactiveRedisConnection connection = context.getBean(ReactiveRedisConnectionFactory.class) + .getReactiveConnection(); + if (expectedConfig.length > 0) { + assertThat(connection.serverCommands().getConfig("*").block(Duration.ofSeconds(30))) + .contains(expectedConfig); + } + }; + } + + static class MaxEntriesReactiveRedisAction implements ConfigureReactiveRedisAction { + + @Override + public Mono configure(ReactiveRedisConnection connection) { + return Mono.when(connection.serverCommands().setConfig("set-max-intset-entries", "1024")); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java index 94adbae6f1..b380d49508 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.session; 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.boot.context.properties.PropertyMapper; @@ -28,7 +29,11 @@ import org.springframework.context.annotation.Import; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.config.ReactiveSessionRepositoryCustomizer; +import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository; import org.springframework.session.data.redis.ReactiveRedisSessionRepository; +import org.springframework.session.data.redis.config.ConfigureReactiveRedisAction; +import org.springframework.session.data.redis.config.annotation.ConfigureNotifyKeyspaceEventsReactiveAction; +import org.springframework.session.data.redis.config.annotation.web.server.RedisIndexedWebSessionConfiguration; import org.springframework.session.data.redis.config.annotation.web.server.RedisWebSessionConfiguration; /** @@ -43,20 +48,58 @@ import org.springframework.session.data.redis.config.annotation.web.server.Redis @ConditionalOnMissingBean(ReactiveSessionRepository.class) @ConditionalOnBean(ReactiveRedisConnectionFactory.class) @EnableConfigurationProperties(RedisSessionProperties.class) -@Import(RedisWebSessionConfiguration.class) class RedisReactiveSessionConfiguration { - @Bean - ReactiveSessionRepositoryCustomizer springBootSessionRepositoryCustomizer( - SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, - ServerProperties serverProperties) { - return (sessionRepository) -> { - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(sessionProperties.determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout())) - .to(sessionRepository::setDefaultMaxInactiveInterval); - map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace); - map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode); - }; + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.session.redis", name = "repository-type", havingValue = "default", + matchIfMissing = true) + @Import(RedisWebSessionConfiguration.class) + static class DefaultRedisSessionConfiguration { + + @Bean + ReactiveSessionRepositoryCustomizer springBootSessionRepositoryCustomizer( + SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, + ServerProperties serverProperties) { + return (sessionRepository) -> { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(sessionProperties + .determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout())) + .to(sessionRepository::setDefaultMaxInactiveInterval); + map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace); + map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode); + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.session.redis", name = "repository-type", havingValue = "indexed") + @Import(RedisIndexedWebSessionConfiguration.class) + static class IndexedRedisSessionConfiguration { + + @Bean + @ConditionalOnMissingBean + ConfigureReactiveRedisAction configureReactiveRedisAction(RedisSessionProperties redisSessionProperties) { + return switch (redisSessionProperties.getConfigureAction()) { + case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsReactiveAction(); + case NONE -> ConfigureReactiveRedisAction.NO_OP; + }; + } + + @Bean + ReactiveSessionRepositoryCustomizer springBootSessionRepositoryCustomizer( + SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, + ServerProperties serverProperties) { + return (sessionRepository) -> { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(sessionProperties + .determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout())) + .to(sessionRepository::setDefaultMaxInactiveInterval); + map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace); + map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode); + }; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionProperties.java index 8b2042567a..93f16dc5ca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -36,7 +36,7 @@ public class RedisSessionProperties { /** * Sessions flush mode. Determines when session changes are written to the session - * store. + * store. Not supported with a reactive session repository. */ private FlushMode flushMode = FlushMode.ON_SAVE; @@ -47,14 +47,15 @@ public class RedisSessionProperties { private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE; /** - * The configure action to apply when no user defined ConfigureRedisAction bean is - * present. + * The configure action to apply when no user-defined ConfigureRedisAction or + * ConfigureReactiveRedisAction bean is present. */ private ConfigureAction configureAction = ConfigureAction.NOTIFY_KEYSPACE_EVENTS; /** * Cron expression for expired session cleanup job. Only supported when - * repository-type is set to indexed. + * repository-type is set to indexed. Not supported with a reactive session + * repository. */ private String cleanupCron; @@ -135,12 +136,13 @@ public class RedisSessionProperties { public enum RepositoryType { /** - * Auto-configure a RedisSessionRepository. + * Auto-configure a RedisSessionRepository or ReactiveRedisSessionRepository. */ DEFAULT, /** - * Auto-configure a RedisIndexedSessionRepository. + * Auto-configure a RedisIndexedSessionRepository or + * ReactiveRedisIndexedSessionRepository. */ INDEXED