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 20f4c9ea8f..1b3301f6e3 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 @@ -36,6 +36,7 @@ import org.springframework.util.StringUtils; * @author Mark Paluch * @author Stephane Nicoll * @author Alen Turkovic + * @author Scott Frederick */ abstract class RedisConnectionConfiguration { @@ -135,7 +136,11 @@ abstract class RedisConnectionConfiguration { protected ConnectionInfo parseUrl(String url) { try { URI uri = new URI(url); - boolean useSsl = (url.startsWith("rediss://")); + String scheme = uri.getScheme(); + if (!"redis".equals(scheme) && !"rediss".equals(scheme)) { + throw new RedisUrlSyntaxException(url); + } + boolean useSsl = ("rediss".equals(scheme)); String password = null; if (uri.getUserInfo() != null) { password = uri.getUserInfo(); @@ -147,7 +152,7 @@ abstract class RedisConnectionConfiguration { return new ConnectionInfo(uri, useSsl, password); } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Malformed url '" + url + "'", ex); + throw new RedisUrlSyntaxException(url, ex); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxException.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxException.java new file mode 100644 index 0000000000..d9087678fa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 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; + +/** + * Exception thrown when a Redis URL is malformed or invalid. + * + * @author Scott Frederick + */ +class RedisUrlSyntaxException extends RuntimeException { + + private final String url; + + RedisUrlSyntaxException(String url, Exception cause) { + super(buildMessage(url), cause); + this.url = url; + } + + RedisUrlSyntaxException(String url) { + super(buildMessage(url)); + this.url = url; + } + + String getUrl() { + return this.url; + } + + private static String buildMessage(String url) { + return "Invalid Redis URL '" + url + "'"; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzer.java new file mode 100644 index 0000000000..7c485a3df7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzer.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2020 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 java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * A {@code FailureAnalyzer} that performs analysis of failures caused by a + * {@link RedisUrlSyntaxException}. + * + * @author Scott Frederick + */ +class RedisUrlSyntaxFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, RedisUrlSyntaxException cause) { + try { + URI uri = new URI(cause.getUrl()); + if ("redis-sentinel".equals(uri.getScheme())) { + return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()), + "Use spring.redis.sentinel properties instead of spring.redis.url to configure Redis sentinel addresses.", + cause); + } + if ("redis-socket".equals(uri.getScheme())) { + return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()), + "Configure the appropriate Spring Data Redis connection beans directly instead of setting the property 'spring.redis.url'.", + cause); + } + if (!"redis".equals(uri.getScheme()) && !"rediss".equals(uri.getScheme())) { + return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()), + "Use the scheme 'redis://` for insecure or `rediss://` for secure Redis standalone configuration.", + cause); + } + } + catch (URISyntaxException ex) { + // fall through to default description and action + } + return new FailureAnalysis(getDefaultDescription(cause.getUrl()), + "Review the value of the property 'spring.redis.url'.", cause); + } + + private String getDefaultDescription(String url) { + return "The URL '" + url + "' is not valid for configuring Spring Data Redis. "; + } + + private String getUnsupportedSchemeDescription(String url, String scheme) { + return getDefaultDescription(url) + "The scheme '" + scheme + "' is not supported."; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 6526dcdc8b..eb6f3f7f90 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -149,6 +149,7 @@ org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAuto # Failure analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ 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 b808025520..f6f0c827b1 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 @@ -48,6 +48,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link RedisAutoConfiguration}. @@ -60,6 +61,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Mark Paluch * @author Stephane Nicoll * @author Alen Turkovic + * @author Scott Frederick */ class RedisAutoConfigurationTests { @@ -228,6 +230,17 @@ class RedisAutoConfigurationTests { }); } + @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-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzerTests.java new file mode 100644 index 0000000000..6a1b41f098 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzerTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2020 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.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RedisUrlSyntaxFailureAnalyzer}. + * + * @author Scott Frederick + */ +class RedisUrlSyntaxFailureAnalyzerTests { + + @Test + void analyzeInvalidUrlSyntax() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException("redis://invalid"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains("The URL 'redis://invalid' is not valid"); + assertThat(analysis.getAction()).contains("Review the value of the property 'spring.redis.url'"); + } + + @Test + void analyzeRedisHttpUrl() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException("http://127.0.0.1:26379/mymaster"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains("The URL 'http://127.0.0.1:26379/mymaster' is not valid") + .contains("The scheme 'http' is not supported"); + assertThat(analysis.getAction()).contains("Use the scheme 'redis://` for insecure or `rediss://` for secure"); + } + + @Test + void analyzeRedisSentinelUrl() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException( + "redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains( + "The URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster' is not valid") + .contains("The scheme 'redis-sentinel' is not supported"); + assertThat(analysis.getAction()).contains("Use spring.redis.sentinel properties"); + } + + @Test + void analyzeRedisSocketUrl() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException("redis-socket:///redis/redis.sock"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains("The URL 'redis-socket:///redis/redis.sock' is not valid") + .contains("The scheme 'redis-socket' is not supported"); + assertThat(analysis.getAction()).contains("Configure the appropriate Spring Data Redis connection beans"); + } + +}