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..6c47e9c3bdb 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,8 +22,10 @@ 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.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -32,6 +34,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 +53,71 @@ 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-type", havingValue = "default", + matchIfMissing = true) + static class DefaultRedisSessionConfiguration { + + @Configuration(proxyBeanMethods = false) + static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration { + + @Autowired + void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, + ServerProperties serverProperties) { + String cleanupCron = redisSessionProperties.getCleanupCron(); + if (cleanupCron != null) { + throw new InvalidConfigurationPropertyValueException("spring.session.redis.cleanup-cron", + cleanupCron, + "Cron-based cleanup is only supported when spring.session.redis.repository-type is set to " + + "indexed."); + } + 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-type", 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) + static class SpringBootRedisIndexedHttpSessionConfiguration extends RedisIndexedHttpSessionConfiguration { + + private static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; + + @Autowired + 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()); + String cleanupCron = redisSessionProperties.getCleanupCron(); + setCleanupCron((cleanupCron != null) ? cleanupCron : DEFAULT_CLEANUP_CRON); } - setRedisNamespace(redisSessionProperties.getNamespace()); - setFlushMode(redisSessionProperties.getFlushMode()); - setSaveMode(redisSessionProperties.getSaveMode()); - setCleanupCron(redisSessionProperties.getCleanupCron()); + } } 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 69d66a0bb1e..8b2042567ae 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-2019 the original author or authors. + * Copyright 2012-2022 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. @@ -29,8 +29,6 @@ import org.springframework.session.SaveMode; @ConfigurationProperties(prefix = "spring.session.redis") public class RedisSessionProperties { - private static final String DEFAULT_CLEANUP_CRON = "0 * * * * *"; - /** * Namespace for keys used to store sessions. */ @@ -55,9 +53,15 @@ public class RedisSessionProperties { private ConfigureAction configureAction = ConfigureAction.NOTIFY_KEYSPACE_EVENTS; /** - * Cron expression for expired session cleanup job. + * Cron expression for expired session cleanup job. Only supported when + * repository-type is set to indexed. */ - private String cleanupCron = DEFAULT_CLEANUP_CRON; + private String cleanupCron; + + /** + * Type of Redis session repository to configure. + */ + private RepositoryType repositoryType = RepositoryType.DEFAULT; public String getNamespace() { return this.namespace; @@ -99,6 +103,14 @@ public class RedisSessionProperties { this.configureAction = configureAction; } + public RepositoryType getRepositoryType() { + return this.repositoryType; + } + + public void setRepositoryType(RepositoryType repositoryType) { + this.repositoryType = repositoryType; + } + /** * Strategies for configuring and validating Redis. */ @@ -117,4 +129,21 @@ public class RedisSessionProperties { } + /** + * Type of Redis session repository to auto-configure. + */ + public enum RepositoryType { + + /** + * Auto-configure a RedisSessionRepository. + */ + DEFAULT, + + /** + * Auto-configure a RedisIndexedSessionRepository. + */ + INDEXED + + } + } 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..2debd65b487 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 @@ -2749,6 +2749,10 @@ "name": "spring.session.jdbc.save-mode", "defaultValue": "on-set-attribute" }, + { + "name": "spring.session.redis.cleanup-cron", + "defaultValue": "0 * * * * *" + }, { "name": "spring.session.redis.configure-action", "defaultValue": "notify-keyspace-events" @@ -2757,6 +2761,10 @@ "name": "spring.session.redis.flush-mode", "defaultValue": "on-save" }, + { + "name": "spring.session.redis.repository-type", + "defaultValue": "default" + }, { "name": "spring.session.redis.save-mode", "defaultValue": "on-set-attribute" @@ -3362,4 +3370,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..f27fec99e52 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,8 +25,9 @@ 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.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -38,6 +39,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 +72,19 @@ 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 + void invalidConfigurationPropertyValueWhenDefaultConfigIsUsedWithCustomCronCleanup() { + this.contextRunner.withPropertyValues("spring.data.redis.host=" + redis.getHost(), + "spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.redis.cleanup-cron=0 0 * * * *") + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasRootCauseExactlyInstanceOf(InvalidConfigurationPropertyValueException.class); + }); } @Test @@ -79,8 +92,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 +102,98 @@ 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-type=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-type=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-type=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-type=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-type=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..0cecb91d273 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-type=indexed