diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index 9d7f20cb3d73..02cb7fc65f85 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -16,10 +16,13 @@ package org.springframework.boot.autoconfigure.data.redis; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Duration; import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; import io.lettuce.core.SocketOptions; import io.lettuce.core.TimeoutOptions; import io.lettuce.core.cluster.ClusterClientOptions; @@ -40,11 +43,13 @@ import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSocketConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -58,11 +63,15 @@ @ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true) class LettuceConnectionConfiguration extends RedisConnectionConfiguration { + private final RedisSocketConfiguration socketConfiguration; + LettuceConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, + ObjectProvider socketConfigurationProvider, ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider) { super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider); + this.socketConfiguration = socketConfigurationProvider.getIfAvailable(); } @Bean(destroyMethod = "shutdown") @@ -90,9 +99,72 @@ private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientCon if (getClusterConfiguration() != null) { return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration); } + if (getSocketConfiguration() != null) { + return new LettuceConnectionFactory(getSocketConfiguration(), clientConfiguration); + } return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration); } + @Override + RedisSentinelConfiguration maybeGetCustomSentinelConfig() { + if (urlConfiguredForSentinelScheme(this.getProperties().getUrl())) { + RedisURI redisURI = createRedisUri(this.getProperties().getUrl()); + return (RedisSentinelConfiguration) LettuceConnectionFactory.createRedisConfiguration(redisURI); + } + return null; + } + + @Override + RedisStandaloneConfiguration maybeGetCustomStandaloneConfig() { + if (urlConfiguredForNonStandardStandaloneScheme(this.getProperties().getUrl())) { + RedisURI redisURI = createRedisUri(this.getProperties().getUrl()); + return (RedisStandaloneConfiguration) LettuceConnectionFactory.createRedisConfiguration(redisURI); + } + return null; + } + + private RedisSocketConfiguration getSocketConfiguration() { + if (this.socketConfiguration != null) { + return this.socketConfiguration; + } + if (urlConfiguredForSocketScheme(this.getProperties().getUrl())) { + RedisURI redisURI = createRedisUri(this.getProperties().getUrl()); + return (RedisSocketConfiguration) LettuceConnectionFactory.createRedisConfiguration(redisURI); + } + return null; + } + + private boolean urlConfiguredForSentinelScheme(String url) { + String scheme = getUrlScheme(url); + return scheme != null && RedisURI.URI_SCHEME_REDIS_SENTINEL.equals(scheme) + || RedisURI.URI_SCHEME_REDIS_SENTINEL_SECURE.equals(scheme); + } + + private boolean urlConfiguredForSocketScheme(String url) { + String scheme = getUrlScheme(url); + return scheme != null && (RedisURI.URI_SCHEME_REDIS_SOCKET.equals(scheme) + || RedisURI.URI_SCHEME_REDIS_SOCKET_ALT.equals(scheme)); + } + + private boolean urlConfiguredForNonStandardStandaloneScheme(String url) { + String scheme = getUrlScheme(url); + return scheme != null && !RedisURI.URI_SCHEME_REDIS.equals(scheme) + && !RedisURI.URI_SCHEME_REDIS_SECURE.equals(scheme); + } + + private String getUrlScheme(String url) { + if (!StringUtils.hasText(url)) { + return null; + } + try { + URI uri = new URI(url); + return uri.getScheme(); + } + catch (URISyntaxException ex) { + throw new RedisUrlSyntaxException(url, ex); + } + } + private LettuceClientConfiguration getLettuceClientConfiguration( ObjectProvider builderCustomizers, ClientResources clientResources, Pool pool) { @@ -161,10 +233,20 @@ private ClientOptions.Builder initializeClientOptionsBuilder() { } private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) { - ConnectionInfo connectionInfo = parseUrl(getProperties().getUrl()); - if (connectionInfo.isUseSsl()) { - builder.useSsl(); + RedisURI redisURI = createRedisUri(getProperties().getUrl()); + builder.apply(redisURI); + } + + private RedisURI createRedisUri(String uri) { + RedisURI redisURI = RedisURI.create(uri); + // Set the sentinel password from properties on the sentinel nodes as there is no + // way to set that on the url + if (!ObjectUtils.isEmpty(redisURI.getSentinels()) && getProperties().getSentinel() != null + && StringUtils.hasText(getProperties().getSentinel().getPassword())) { + redisURI.getSentinels().forEach((sentinelNodeRedisUri) -> sentinelNodeRedisUri + .setPassword(getProperties().getSentinel().getPassword().toCharArray())); } + return redisURI; } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java index b870e4a5508b..4c134a87af10 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java @@ -68,6 +68,12 @@ protected final RedisStandaloneConfiguration getStandaloneConfig() { if (this.standaloneConfiguration != null) { return this.standaloneConfiguration; } + + RedisStandaloneConfiguration customConfig = maybeGetCustomStandaloneConfig(); + if (customConfig != null) { + return customConfig; + } + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); if (StringUtils.hasText(this.properties.getUrl())) { ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl()); @@ -86,10 +92,20 @@ protected final RedisStandaloneConfiguration getStandaloneConfig() { return config; } + RedisStandaloneConfiguration maybeGetCustomStandaloneConfig() { + return null; + } + protected final RedisSentinelConfiguration getSentinelConfig() { if (this.sentinelConfiguration != null) { return this.sentinelConfiguration; } + + RedisSentinelConfiguration customConfig = maybeGetCustomSentinelConfig(); + if (customConfig != null) { + return customConfig; + } + RedisProperties.Sentinel sentinelProperties = this.properties.getSentinel(); if (sentinelProperties != null) { RedisSentinelConfiguration config = new RedisSentinelConfiguration(); @@ -108,6 +124,10 @@ protected final RedisSentinelConfiguration getSentinelConfig() { return null; } + RedisSentinelConfiguration maybeGetCustomSentinelConfig() { + return null; + } + /** * Create a {@link RedisClusterConfiguration} if necessary. * @return {@literal null} if no cluster settings are set. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java index 4a834b40bb47..421169ecf08e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java @@ -32,6 +32,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link RedisAutoConfiguration} when Lettuce is not on the classpath. @@ -39,6 +40,7 @@ * @author Mark Paluch * @author Stephane Nicoll * @author Weix Sun + * @author Chris Bono */ @ClassPathExclusions("lettuce-core-*.jar") class RedisAutoConfigurationJedisTests { @@ -209,6 +211,17 @@ void testRedisConfigurationWithSentinelAndAuthentication() { }); } + @Test + void testRedisSentinelUrlConfiguration() { + this.contextRunner + .withPropertyValues( + "spring.redis.url=redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster") + .run((context) -> assertThatIllegalStateException() + .isThrownBy(() -> context.getBean(JedisConnectionFactory.class)) + .withRootCauseInstanceOf(RedisUrlSyntaxException.class).havingRootCause().withMessageContaining( + "Invalid Redis URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster'")); + } + @Test void testRedisConfigurationWithCluster() { this.contextRunner.withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationLettuceUrlTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationLettuceUrlTests.java new file mode 100644 index 000000000000..914d0af381e3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationLettuceUrlTests.java @@ -0,0 +1,314 @@ +/* + * Copyright 2012-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.redis; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSocketConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests {@link RedisAutoConfiguration} when using {@code Lettuce} and configuring via the + * {@code spring.redis.url} property. + * + * @author Chris Bono + */ +class RedisAutoConfigurationLettuceUrlTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)); + + private String getUserName(LettuceConnectionFactory factory) { + return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername"); + } + + @Nested + class RedisStandaloneUrlWithStandardScheme { + + @Test + void withMinimalFields() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis://host1:6379").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(6379); + assertThat(getUserName(cf)).isNullOrEmpty(); + assertThat(cf.getPassword()).isNullOrEmpty(); + assertThat(cf.isUseSsl()).isFalse(); + }); + } + + @Test + void withAllFields() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis://user:password@host1:33").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.isUseSsl()).isFalse(); + }); + } + + @Test + void withSsl() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=rediss://user:password@host1:33").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void withoutUsernameWithPasswordContainingColon() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis://:pass:word@host1:33").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isNullOrEmpty(); + assertThat(cf.getPassword()).isEqualTo("pass:word"); + }); + } + + @Test + void withUsernameWithPasswordStartsWithColonAndContainsColon() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis://user::pass:word@host1:33").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo(":pass:word"); + }); + } + + } + + @Nested + class RedisStandaloneUrlWithNonStandardScheme { + + @Test + void withAltSsl() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis+ssl://user:password@host1:33/7").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void withAltTls() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis+tls://user:password@host1:33/7").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.isUseSsl()).isTrue(); + assertThat(cf.isStartTls()).isTrue(); + }); + } + + @Test + void withPropsSetOnUrlIgnoresPropsSetInConfig() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis+ssl://user:password@host1:33/7", + "spring.redis.username=abc", "spring.redis.password=xyz", "spring.redis.host=foo", + "spring.redis.port=1000", "spring.redis.database=2", "spring.redis.ssl=false") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("host1"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void withClientOptionPropsSetOnUrlOverridesAndRespectsPropsSetInConfig() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis+ssl://host1?timeout=47s&clientName=zuser", + "spring.redis.timeout=1200", "spring.redis.connect-timeout=2400", + "spring.redis.lettuce.shutdown-timeout=3600") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getClientName()).isEqualTo("zuser"); + assertThat(cf.getTimeout()).isEqualTo(47000); + assertThat(cf.getShutdownTimeout()).isEqualTo(3600); + assertThat(cf.getClientConfiguration().getClientOptions().get().getSocketOptions() + .getConnectTimeout().toMillis()).isEqualTo(2400); + }); + } + + @Test + void withoutClientOptionPropsSetOnUrlUsesDefaultCientOptions() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis+ssl://host1").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getClientName()).isNullOrEmpty(); + assertThat(cf.getTimeout()).isEqualTo(60000); + assertThat(cf.getShutdownTimeout()).isEqualTo(100); + assertThat(cf.getClientConfiguration().getClientOptions().get().getSocketOptions() + .getConnectTimeout().toMillis()).isEqualTo(10000); + }); + } + + } + + @Nested + class RedisSentinelUrl { + + @Test + void withMinimalFields() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis-sentinel://127.0.0.1?sentinelMasterId=5150") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(cf)).isNullOrEmpty(); + assertThat(cf.getPassword()).isNullOrEmpty(); + assertThat(cf.getDatabase()).isEqualTo(0); + assertThat(cf.isUseSsl()).isFalse(); + assertThat(cf.isRedisSentinelAware()).isTrue(); + RedisSentinelConfiguration sentinelConfiguration = cf.getSentinelConfiguration(); + assertThat(sentinelConfiguration.getSentinels()).flatMap(Object::toString) + .containsExactlyInAnyOrder("127.0.0.1:26379"); + assertThat(sentinelConfiguration.getMaster().getName()).isEqualTo("5150"); + assertThat(sentinelConfiguration.getSentinelPassword().isPresent()).isFalse(); + }); + } + + @Test + void withAllFields() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner.withPropertyValues( + "spring.redis.url=redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/7?sentinelMasterId=5150", + "spring.redis.sentinel.password: secret").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(cf)).isEqualTo("username"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.isUseSsl()).isFalse(); + assertThat(cf.isRedisSentinelAware()).isTrue(); + RedisSentinelConfiguration sentinelConfiguration = cf.getSentinelConfiguration(); + assertThat(sentinelConfiguration.getSentinels()).flatMap(Object::toString) + .containsExactlyInAnyOrder("127.0.0.1:26379", "127.0.0.1:26380"); + assertThat(sentinelConfiguration.getMaster().getName()).isEqualTo("5150"); + assertThat(sentinelConfiguration.getSentinelPassword().get()).isEqualTo("secret".toCharArray()); + }); + + } + + @Test + void withSsl() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=rediss-sentinel://127.0.0.1?sentinelMasterId=5150") + .run((context) -> { + LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + assertThat(connectionFactory.isRedisSentinelAware()).isTrue(); + assertThat(connectionFactory.isUseSsl()).isTrue(); + }); + } + + @Test + void withPropsSetOnUrlIgnoresPropsSetInConfig() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner.withPropertyValues( + "spring.redis.url=redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/7?sentinelMasterId=5150", + "spring.redis.sentinel.master=mymaster", "spring.redis.sentinel.nodes=server1:111, server2:222") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(cf)).isEqualTo("username"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.isUseSsl()).isFalse(); + assertThat(cf.isRedisSentinelAware()).isTrue(); + RedisSentinelConfiguration sentinelConfiguration = cf.getSentinelConfiguration(); + assertThat(sentinelConfiguration.getSentinels()).flatMap(Object::toString) + .containsExactlyInAnyOrder("127.0.0.1:26379", "127.0.0.1:26380"); + assertThat(sentinelConfiguration.getMaster().getName()).isEqualTo("5150"); + }); + } + + } + + @Nested + class RedisSocketUrl { + + @Test + void withMinimalFields() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis-socket:///mysocket").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(cf)).isNullOrEmpty(); + assertThat(cf.getPassword()).isNullOrEmpty(); + assertThat(cf.getDatabase()).isEqualTo(0); + assertThat(cf.getSocketConfiguration()).extracting(RedisSocketConfiguration::getSocket) + .isEqualTo("/mysocket"); + }); + } + + @Test + void withAllFields() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis-socket://user:password@/mysocket?database=7") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.getSocketConfiguration()).extracting(RedisSocketConfiguration::getSocket) + .isEqualTo("/mysocket"); + }); + } + + @Test + void withAltScheme() { + RedisAutoConfigurationLettuceUrlTests.this.contextRunner + .withPropertyValues("spring.redis.url=redis+socket://user:password@/mysocket?database=7") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + assertThat(cf.getDatabase()).isEqualTo(7); + assertThat(cf.getSocketConfiguration()).extracting(RedisSocketConfiguration::getSocket) + .isEqualTo("/mysocket"); + }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index cbc08550358f..0d36a4aa970d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -56,7 +56,6 @@ import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** @@ -72,6 +71,7 @@ * @author Alen Turkovic * @author Scott Frederick * @author Weix Sun + * @author Chris Bono */ class RedisAutoConfigurationTests { @@ -120,57 +120,6 @@ void testCustomizeRedisConfiguration() { }); } - @Test - void testRedisUrlConfiguration() { - this.contextRunner - .withPropertyValues("spring.redis.host:foo", "spring.redis.url:redis://user:password@example:33") - .run((context) -> { - LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); - assertThat(cf.getHostName()).isEqualTo("example"); - assertThat(cf.getPort()).isEqualTo(33); - assertThat(getUserName(cf)).isEqualTo("user"); - assertThat(cf.getPassword()).isEqualTo("password"); - assertThat(cf.isUseSsl()).isFalse(); - }); - } - - @Test - void testOverrideUrlRedisConfiguration() { - this.contextRunner - .withPropertyValues("spring.redis.host:foo", "spring.redis.password:xyz", "spring.redis.port:1000", - "spring.redis.ssl:false", "spring.redis.url:rediss://user:password@example:33") - .run((context) -> { - LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); - assertThat(cf.getHostName()).isEqualTo("example"); - assertThat(cf.getPort()).isEqualTo(33); - assertThat(getUserName(cf)).isEqualTo("user"); - assertThat(cf.getPassword()).isEqualTo("password"); - assertThat(cf.isUseSsl()).isTrue(); - }); - } - - @Test - void testPasswordInUrlWithColon() { - this.contextRunner.withPropertyValues("spring.redis.url:redis://:pass:word@example:33").run((context) -> { - LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); - assertThat(cf.getHostName()).isEqualTo("example"); - assertThat(cf.getPort()).isEqualTo(33); - assertThat(getUserName(cf)).isEqualTo(""); - assertThat(cf.getPassword()).isEqualTo("pass:word"); - }); - } - - @Test - void testPasswordInUrlStartsWithColon() { - this.contextRunner.withPropertyValues("spring.redis.url:redis://user::pass:word@example:33").run((context) -> { - LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); - assertThat(cf.getHostName()).isEqualTo("example"); - assertThat(cf.getPort()).isEqualTo(33); - assertThat(getUserName(cf)).isEqualTo("user"); - assertThat(cf.getPassword()).isEqualTo(":pass:word"); - }); - } - @Test void testRedisConfigurationUsePoolByDefault() { Pool defaultPool = new RedisProperties().getLettuce().getPool(); @@ -322,17 +271,6 @@ void testRedisConfigurationWithSentinelPasswordAndDataNodePassword() { }); } - @Test - void testRedisSentinelUrlConfiguration() { - this.contextRunner - .withPropertyValues( - "spring.redis.url=redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster") - .run((context) -> assertThatIllegalStateException() - .isThrownBy(() -> context.getBean(LettuceConnectionFactory.class)) - .withRootCauseInstanceOf(RedisUrlSyntaxException.class).havingRootCause().withMessageContaining( - "Invalid Redis URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster'")); - } - @Test void testRedisConfigurationWithCluster() { List clusterNodes = Arrays.asList("127.0.0.1:27379", "127.0.0.1:27380"); diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index 18982ebb689c..7135c7103109 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -41,6 +41,39 @@ The following listing shows an example of such a bean: include::{docs-java}/data/nosql/redis/connecting/MyBean.java[] ---- +You can set the configprop:spring.redis.url[] property to change the URL and configure +additional settings as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + redis: + url: "redis://user:secret@redis.example.com:6379" +---- + +Alternatively, you can specify connection details using discrete properties. For example, you might +declare the following settings in your `application.yml`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + redis: + host: "redis.example.com" + port: 6379 + username: "user" + password: "secret" +---- +When using both the URL and the discrete properties, any values that are set in the URL will take precedence over the values set in the properties. + +The url property supports the `redis` and `rediss` schemes as well as the following additional schemes when using Lettuce: + +* `redis+ssl` +* `redis+tls` +* `redis-sentinel` +* `rediss-sentinel` +* `redis-socket` +* `redis+socket` + TIP: You can also register an arbitrary number of beans that implement `LettuceClientConfigurationBuilderCustomizer` for more advanced customizations. `ClientResources` can also be customized using `ClientResourcesBuilderCustomizer`. If you use Jedis, `JedisClientConfigurationBuilderCustomizer` is also available.