From 3093380e356e385ce25594dbfbf74da6207145c6 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Tue, 30 Aug 2022 10:39:00 +0200 Subject: [PATCH] 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 --- .../session/RedisSessionConfiguration.java | 72 ++++++++++++---- ...itional-spring-configuration-metadata.json | 16 +++- .../SessionAutoConfigurationRedisTests.java | 86 +++++++++++++------ .../src/main/resources/application.properties | 1 + 4 files changed, 129 insertions(+), 46 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java index c175afb4028..06c4f6f143c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; 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; @@ -32,6 +33,7 @@ import org.springframework.session.SessionRepository; 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; /** @@ -50,30 +52,62 @@ import org.springframework.session.data.redis.config.annotation.web.http.RedisIn @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 { + @ConditionalOnProperty(prefix = "spring.session.redis", name = "repository", havingValue = "indexed") + static class IndexedRedisSessionConfiguration { - @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()); + @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()); + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 2612f12328a..b8f94f492d9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2757,6 +2757,20 @@ "name": "spring.session.redis.flush-mode", "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", "defaultValue": "on-set-attribute" @@ -3362,4 +3376,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java index 57b48d77f3d..d0fecd42531 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java @@ -25,7 +25,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; 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; @@ -38,6 +38,7 @@ import org.springframework.session.FlushMode; 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; @@ -70,8 +71,8 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio .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 @@ -79,8 +80,8 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio 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 @@ -89,64 +90,97 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio .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.cleanup-cron=0 0 12 * * *", "spring.data.redis.host=" + redis.getHost(), + "spring.session.redis.save-mode=on-get-attribute", "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(validateSpringSessionUsesDefaultRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE)); } @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)) - .withPropertyValues("spring.session.redis.configure-action=none", - "spring.data.redis.host=" + redis.getHost(), + .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(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()) .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 validateSpringSessionUsesRedis( - String sessionCreatedChannelPrefix, FlushMode flushMode, SaveMode saveMode, String cleanupCron) { + private ContextConsumer 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 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); }; } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/main/resources/application.properties index 52ebf796de2..8c8faded23d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/main/resources/application.properties @@ -1,3 +1,4 @@ management.endpoints.web.exposure.include=* spring.security.user.name=user spring.security.user.password=password +spring.session.redis.repository=indexed