From e2a355f0035a699730b2ae242cd68544c2d863be Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 9 Sep 2021 17:50:46 +0100 Subject: [PATCH] Consolidate Elasticsearch configuration properties Previously, a number of Elasticsearch properties were duplicated across the spring.elasticsearch.rest and spring.data.elasticsearch.client.reactive prefixes for configuring the blocking REST client provided by Elasticsearch and the reactive client provided by Spring Data respectively. This could cause problems when using the Elasticsearch REST client configured with a custom spring.elasticsearch.rest.uris. If Spring WebFlux (to make use of WebClient) and Spring Data Elasticsearch were on the classpath, the reactive Elasticsearch Client would be autoconfigured but it would use the default value of its analogous spring.data.elasticsearch.client.reactive.endpoints property. It would be unable to connect, causing a startup failure. This commit consoliates the configuration properties where possible. Each setting that is common across the two clients is now configured using a single, shared spring.elasticsearch property. Each setting that is specific to the blocked REST client or the WebClient-based reactive client now have prefixes of spring.elasticsearch.restclient and spring.elasticsearch.webclient respectively. The old properties beneath spring.elasticsearch.rest and spring.data.elasticsearch.client.reactive have been deprecated. If a any deprecated property is set, all of the new properties are ignored. In other words, to migrate to the new properties, each usage of a now-deprecated property must be updated to use its new replacement instead. Closes gh-23106 --- ...tiveElasticsearchRestClientProperties.java | 150 +++++++++++++ ...sticsearchRestClientAutoConfiguration.java | 203 ++++++++++++++++-- ...tiveElasticsearchRestClientProperties.java | 87 +------- ...atedElasticsearchRestClientProperties.java | 159 ++++++++++++++ .../ElasticsearchProperties.java | 100 +++++++++ ...sticsearchRestClientAutoConfiguration.java | 6 +- ...ElasticsearchRestClientConfigurations.java | 82 +++++-- .../ElasticsearchRestClientProperties.java | 78 +------ ...itional-spring-configuration-metadata.json | 6 + ...ientAutoConfigurationIntegrationTests.java | 9 +- ...earchRestClientAutoConfigurationTests.java | 188 +++++++++++++--- ...ientAutoConfigurationIntegrationTests.java | 3 +- ...earchRestClientAutoConfigurationTests.java | 123 +++++++---- .../src/docs/asciidoc/data/nosql.adoc | 54 +++-- src/checkstyle/checkstyle-suppressions.xml | 3 + 15 files changed, 960 insertions(+), 291 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java new file mode 100644 index 00000000000..cb879c531ab --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/DeprecatedReactiveElasticsearchRestClientProperties.java @@ -0,0 +1,150 @@ +/* + * 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.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.util.unit.DataSize; + +/** + * Deprecated configuration properties for Elasticsearch Reactive REST clients. + * + * @author Brian Clozel + * @deprecated since 2.6.0 for removal in 2.8.0 + */ +@Deprecated +@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") +class DeprecatedReactiveElasticsearchRestClientProperties { + + /** + * Comma-separated list of the Elasticsearch endpoints to connect to. + */ + private List endpoints = new ArrayList<>(Collections.singletonList("localhost:9200")); + + /** + * Whether the client should use SSL to connect to the endpoints. + */ + private boolean useSsl = false; + + /** + * Credentials username. + */ + private String username; + + /** + * Credentials password. + */ + private String password; + + /** + * Connection timeout. + */ + private Duration connectionTimeout; + + /** + * Read and Write Socket timeout. + */ + private Duration socketTimeout; + + /** + * Limit on the number of bytes that can be buffered whenever the input stream needs + * to be aggregated. + */ + private DataSize maxInMemorySize; + + private boolean customized = false; + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.uris") + public List getEndpoints() { + return this.endpoints; + } + + public void setEndpoints(List endpoints) { + this.customized = true; + this.endpoints = endpoints; + } + + @DeprecatedConfigurationProperty(reason = "Use of SSL should be indicated through an https URI scheme") + public boolean isUseSsl() { + return this.useSsl; + } + + public void setUseSsl(boolean useSsl) { + this.customized = true; + this.useSsl = useSsl; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.username") + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.customized = true; + this.username = username; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.password") + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.customized = true; + this.password = password; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.connection-timeout") + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.customized = true; + this.connectionTimeout = connectionTimeout; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.socket-timeout") + public Duration getSocketTimeout() { + return this.socketTimeout; + } + + public void setSocketTimeout(Duration socketTimeout) { + this.customized = true; + this.socketTimeout = socketTimeout; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.webclient.max-in-memory-size") + public DataSize getMaxInMemorySize() { + return this.maxInMemorySize; + } + + public void setMaxInMemorySize(DataSize maxInMemorySize) { + this.customized = true; + this.maxInMemorySize = maxInMemorySize; + } + + boolean isCustomized() { + return this.customized; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java index 3469c533478..753333c2c00 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java @@ -16,11 +16,18 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import reactor.netty.http.client.HttpClient; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; @@ -29,6 +36,7 @@ import org.springframework.data.elasticsearch.client.ClientConfiguration; import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients; import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients.WebClientConfigurationCallback; +import org.springframework.util.Assert; import org.springframework.util.unit.DataSize; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; @@ -40,30 +48,39 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Brian Clozel * @since 2.2.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ReactiveRestClients.class, WebClient.class, HttpClient.class }) -@EnableConfigurationProperties(ReactiveElasticsearchRestClientProperties.class) +@EnableConfigurationProperties({ ElasticsearchProperties.class, ReactiveElasticsearchRestClientProperties.class, + DeprecatedReactiveElasticsearchRestClientProperties.class }) public class ReactiveElasticsearchRestClientAutoConfiguration { + private final ConsolidatedProperties properties; + + ReactiveElasticsearchRestClientAutoConfiguration(ElasticsearchProperties properties, + ReactiveElasticsearchRestClientProperties restClientProperties, + DeprecatedReactiveElasticsearchRestClientProperties reactiveProperties) { + this.properties = new ConsolidatedProperties(properties, restClientProperties, reactiveProperties); + } + @Bean @ConditionalOnMissingBean - public ClientConfiguration clientConfiguration(ReactiveElasticsearchRestClientProperties properties) { + public ClientConfiguration clientConfiguration() { ClientConfiguration.MaybeSecureClientConfigurationBuilder builder = ClientConfiguration.builder() - .connectedTo(properties.getEndpoints().toArray(new String[0])); + .connectedTo(this.properties.getEndpoints().toArray(new String[0])); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties.isUseSsl()).whenTrue().toCall(builder::usingSsl); - map.from(properties.getUsername()).whenHasText() - .to((username) -> builder.withBasicAuth(username, properties.getPassword())); - map.from(properties.getConnectionTimeout()).to(builder::withConnectTimeout); - map.from(properties.getSocketTimeout()).to(builder::withSocketTimeout); - configureExchangeStrategies(map, builder, properties); + map.from(this.properties.isUseSsl()).whenTrue().toCall(builder::usingSsl); + map.from(this.properties.getCredentials()) + .to((credentials) -> builder.withBasicAuth(credentials.getUsername(), credentials.getPassword())); + map.from(this.properties.getConnectionTimeout()).to(builder::withConnectTimeout); + map.from(this.properties.getSocketTimeout()).to(builder::withSocketTimeout); + configureExchangeStrategies(map, builder); return builder.build(); } private void configureExchangeStrategies(PropertyMapper map, - ClientConfiguration.TerminalClientConfigurationBuilder builder, - ReactiveElasticsearchRestClientProperties properties) { - map.from(properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> { + ClientConfiguration.TerminalClientConfigurationBuilder builder) { + map.from(this.properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> { builder.withClientConfigurer(WebClientConfigurationCallback.from((webClient) -> { ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() .codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize)).build(); @@ -78,4 +95,166 @@ public class ReactiveElasticsearchRestClientAutoConfiguration { return ReactiveRestClients.create(clientConfiguration); } + private static final class ConsolidatedProperties { + + private final ElasticsearchProperties properties; + + private final ReactiveElasticsearchRestClientProperties restClientProperties; + + private final DeprecatedReactiveElasticsearchRestClientProperties deprecatedProperties; + + private final List uris; + + private ConsolidatedProperties(ElasticsearchProperties properties, + ReactiveElasticsearchRestClientProperties restClientProperties, + DeprecatedReactiveElasticsearchRestClientProperties deprecatedreactiveProperties) { + this.properties = properties; + this.restClientProperties = restClientProperties; + this.deprecatedProperties = deprecatedreactiveProperties; + this.uris = properties.getUris().stream().map((s) -> s.startsWith("http") ? s : "http://" + s) + .map(URI::create).collect(Collectors.toList()); + } + + private List getEndpoints() { + if (this.deprecatedProperties.isCustomized()) { + return this.deprecatedProperties.getEndpoints(); + } + return this.uris.stream().map((uri) -> uri.getHost() + ":" + uri.getPort()).collect(Collectors.toList()); + } + + private Credentials getCredentials() { + if (this.deprecatedProperties.isCustomized()) { + return Credentials.from(this.deprecatedProperties); + } + Credentials propertyCredentials = Credentials.from(this.properties); + Credentials uriCredentials = Credentials.from(this.properties.getUris()); + if (uriCredentials == null) { + return propertyCredentials; + } + if (propertyCredentials != null && !uriCredentials.equals(propertyCredentials)) { + throw new IllegalArgumentException( + "Credentials from URI user info do not match those from spring.elasticsearch.username and " + + "spring.elasticsearch.password"); + } + return uriCredentials; + + } + + private Duration getConnectionTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getConnectionTimeout() + : this.properties.getConnectionTimeout(); + } + + private Duration getSocketTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getSocketTimeout() + : this.properties.getSocketTimeout(); + } + + private boolean isUseSsl() { + if (this.deprecatedProperties.isCustomized()) { + return this.deprecatedProperties.isUseSsl(); + } + Set schemes = this.uris.stream().map((uri) -> uri.getScheme()).collect(Collectors.toSet()); + Assert.isTrue(schemes.size() == 1, () -> "Configured Elasticsearch URIs have varying schemes"); + return schemes.iterator().next().equals("https"); + } + + private DataSize getMaxInMemorySize() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getMaxInMemorySize() + : this.restClientProperties.getMaxInMemorySize(); + } + + private static final class Credentials { + + private final String username; + + private final String password; + + private Credentials(String username, String password) { + this.username = username; + this.password = password; + } + + private String getUsername() { + return this.username; + } + + private String getPassword() { + return this.password; + } + + private static Credentials from(List uris) { + Set userInfos = uris.stream().map(URI::create).map((uri) -> uri.getUserInfo()) + .collect(Collectors.toSet()); + Assert.isTrue(userInfos.size() == 1, () -> "Configured Elasticsearch URIs have varying user infos"); + String userInfo = userInfos.iterator().next(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + return new Credentials(parts[0], (parts.length == 2) ? parts[1] : ""); + } + return null; + } + + private static Credentials from(ElasticsearchProperties properties) { + String username = properties.getUsername(); + String password = properties.getPassword(); + if (username == null && password == null) { + return null; + } + return new Credentials(username, password); + } + + private static Credentials from(DeprecatedReactiveElasticsearchRestClientProperties properties) { + String username = properties.getUsername(); + String password = properties.getPassword(); + if (username == null && password == null) { + return null; + } + return new Credentials(username, password); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Credentials other = (Credentials) obj; + if (this.password == null) { + if (other.password != null) { + return false; + } + } + else if (!this.password.equals(other.password)) { + return false; + } + if (this.username == null) { + if (other.username != null) { + return false; + } + } + else if (!this.username.equals(other.username)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.password == null) ? 0 : this.password.hashCode()); + result = prime * result + ((this.username == null) ? 0 : this.username.hashCode()); + return result; + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java index a4a6de83c11..1673f1c2f81 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -16,11 +16,6 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.unit.DataSize; @@ -30,93 +25,15 @@ import org.springframework.util.unit.DataSize; * @author Brian Clozel * @since 2.2.0 */ -@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") +@ConfigurationProperties(prefix = "spring.elasticsearch.webclient") public class ReactiveElasticsearchRestClientProperties { - /** - * Comma-separated list of the Elasticsearch endpoints to connect to. - */ - private List endpoints = new ArrayList<>(Collections.singletonList("localhost:9200")); - - /** - * Whether the client should use SSL to connect to the endpoints. - */ - private boolean useSsl = false; - - /** - * Credentials username. - */ - private String username; - - /** - * Credentials password. - */ - private String password; - - /** - * Connection timeout. - */ - private Duration connectionTimeout; - - /** - * Read and Write Socket timeout. - */ - private Duration socketTimeout; - /** * Limit on the number of bytes that can be buffered whenever the input stream needs * to be aggregated. */ private DataSize maxInMemorySize; - public List getEndpoints() { - return this.endpoints; - } - - public void setEndpoints(List endpoints) { - this.endpoints = endpoints; - } - - public boolean isUseSsl() { - return this.useSsl; - } - - public void setUseSsl(boolean useSsl) { - this.useSsl = useSsl; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getSocketTimeout() { - return this.socketTimeout; - } - - public void setSocketTimeout(Duration socketTimeout) { - this.socketTimeout = socketTimeout; - } - public DataSize getMaxInMemorySize() { return this.maxInMemorySize; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java new file mode 100644 index 00000000000..373bc00c60e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/DeprecatedElasticsearchRestClientProperties.java @@ -0,0 +1,159 @@ +/* + * 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.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; + +/** + * Deprecated configuration properties for Elasticsearch REST clients. + * + * @author Brian Clozel + * @deprecated since 2.6.0 for removal in 2.8.0. + */ +@ConfigurationProperties(prefix = "spring.elasticsearch.rest") +@Deprecated +class DeprecatedElasticsearchRestClientProperties { + + /** + * Comma-separated list of the Elasticsearch instances to use. + */ + private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); + + /** + * Credentials username. + */ + private String username; + + /** + * Credentials password. + */ + private String password; + + /** + * Connection timeout. + */ + private Duration connectionTimeout = Duration.ofSeconds(1); + + /** + * Read timeout. + */ + private Duration readTimeout = Duration.ofSeconds(30); + + private final Sniffer sniffer = new Sniffer(); + + private boolean customized = false; + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.uris") + public List getUris() { + return this.uris; + } + + public void setUris(List uris) { + this.customized = true; + this.uris = uris; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.username") + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.customized = true; + this.username = username; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.password") + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.customized = true; + this.password = password; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.connection-timeout") + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.customized = true; + this.connectionTimeout = connectionTimeout; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.socket-timeout") + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.customized = true; + this.readTimeout = readTimeout; + } + + boolean isCustomized() { + return this.customized; + } + + public Sniffer getSniffer() { + return this.sniffer; + } + + @Deprecated + class Sniffer { + + /** + * Interval between consecutive ordinary sniff executions. + */ + private Duration interval = Duration.ofMinutes(5); + + /** + * Delay of a sniff execution scheduled after a failure. + */ + private Duration delayAfterFailure = Duration.ofMinutes(1); + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.restclient.sniffer.interval") + public Duration getInterval() { + return this.interval; + } + + public void setInterval(Duration interval) { + DeprecatedElasticsearchRestClientProperties.this.customized = true; + this.interval = interval; + } + + @DeprecatedConfigurationProperty(replacement = "spring.elasticsearch.restclient.sniffer.delay-after-failure") + public Duration getDelayAfterFailure() { + return this.delayAfterFailure; + } + + public void setDelayAfterFailure(Duration delayAfterFailure) { + DeprecatedElasticsearchRestClientProperties.this.customized = true; + this.delayAfterFailure = delayAfterFailure; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java new file mode 100644 index 00000000000..738a7936296 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java @@ -0,0 +1,100 @@ +/* + * 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.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Elasticsearch. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +@ConfigurationProperties("spring.elasticsearch") +public class ElasticsearchProperties { + + /** + * Comma-separated list of the Elasticsearch instances to use. + */ + private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); + + /** + * Username for authentication with Elasticsearch. + */ + private String username; + + /** + * Password for authentication with Elasticsearch. + */ + private String password; + + /** + * Connection timeout used when communicating with Elasticsearch. + */ + private Duration connectionTimeout = Duration.ofSeconds(1); + + /** + * Socket timeout used when communicating with Elasticsearch. + */ + private Duration socketTimeout = Duration.ofSeconds(30); + + public List getUris() { + return this.uris; + } + + public void setUris(List uris) { + this.uris = uris; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getSocketTimeout() { + return this.socketTimeout; + } + + public void setSocketTimeout(Duration socketTimeout) { + this.socketTimeout = socketTimeout; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java index 9894fd5890c..e96e6cc5b0b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -36,10 +36,12 @@ import org.springframework.context.annotation.Import; * @author Stephane Nicoll * @since 2.1.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RestHighLevelClient.class) @ConditionalOnMissingBean(RestClient.class) -@EnableConfigurationProperties(ElasticsearchRestClientProperties.class) +@EnableConfigurationProperties({ ElasticsearchProperties.class, ElasticsearchRestClientProperties.class, + DeprecatedElasticsearchRestClientProperties.class }) @Import({ RestClientBuilderConfiguration.class, RestHighLevelClientConfiguration.class, RestClientSnifferConfiguration.class }) public class ElasticsearchRestClientAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java index 33faaa3ff03..e84a5c9cca9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.net.URI; import java.net.URISyntaxException; import java.time.Duration; +import java.util.List; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -53,15 +54,23 @@ class ElasticsearchRestClientConfigurations { @ConditionalOnMissingBean(RestClientBuilder.class) static class RestClientBuilderConfiguration { - @Bean - RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { - return new DefaultRestClientBuilderCustomizer(properties); + private final ConsolidatedProperties properties; + + @SuppressWarnings("deprecation") + RestClientBuilderConfiguration(ElasticsearchProperties properties, + DeprecatedElasticsearchRestClientProperties deprecatedProperties) { + this.properties = new ConsolidatedProperties(properties, deprecatedProperties); } @Bean - RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, + RestClientBuilderCustomizer defaultRestClientBuilderCustomizer() { + return new DefaultRestClientBuilderCustomizer(this.properties); + } + + @Bean + RestClientBuilder elasticsearchRestClientBuilder( ObjectProvider builderCustomizers) { - HttpHost[] hosts = properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); + HttpHost[] hosts = this.properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); RestClientBuilder builder = RestClient.builder(hosts); builder.setHttpClientConfigCallback((httpClientBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); @@ -117,13 +126,18 @@ class ElasticsearchRestClientConfigurations { @Bean @ConditionalOnMissingBean - Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties) { + @SuppressWarnings("deprecation") + Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties, + DeprecatedElasticsearchRestClientProperties deprecatedProperties) { SnifferBuilder builder = Sniffer.builder(client.getLowLevelClient()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties.getSniffer().getInterval()).asInt(Duration::toMillis) - .to(builder::setSniffIntervalMillis); - map.from(properties.getSniffer().getDelayAfterFailure()).asInt(Duration::toMillis) - .to(builder::setSniffAfterFailureDelayMillis); + Duration interval = deprecatedProperties.isCustomized() ? deprecatedProperties.getSniffer().getInterval() + : properties.getSniffer().getInterval(); + map.from(interval).asInt(Duration::toMillis).to(builder::setSniffIntervalMillis); + Duration delayAfterFailure = deprecatedProperties.isCustomized() + ? deprecatedProperties.getSniffer().getDelayAfterFailure() + : properties.getSniffer().getDelayAfterFailure(); + map.from(delayAfterFailure).asInt(Duration::toMillis).to(builder::setSniffAfterFailureDelayMillis); return builder.build(); } @@ -133,9 +147,9 @@ class ElasticsearchRestClientConfigurations { private static final PropertyMapper map = PropertyMapper.get(); - private final ElasticsearchRestClientProperties properties; + private final ConsolidatedProperties properties; - DefaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { + DefaultRestClientBuilderCustomizer(ConsolidatedProperties properties) { this.properties = properties; } @@ -152,7 +166,7 @@ class ElasticsearchRestClientConfigurations { public void customize(RequestConfig.Builder builder) { map.from(this.properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::setConnectTimeout); - map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) + map.from(this.properties::getSocketTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::setSocketTimeout); } @@ -160,7 +174,7 @@ class ElasticsearchRestClientConfigurations { private static class PropertiesCredentialsProvider extends BasicCredentialsProvider { - PropertiesCredentialsProvider(ElasticsearchRestClientProperties properties) { + PropertiesCredentialsProvider(ConsolidatedProperties properties) { if (StringUtils.hasText(properties.getUsername())) { Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), properties.getPassword()); @@ -201,4 +215,44 @@ class ElasticsearchRestClientConfigurations { } + @SuppressWarnings("deprecation") + private static final class ConsolidatedProperties { + + private final ElasticsearchProperties properties; + + private final DeprecatedElasticsearchRestClientProperties deprecatedProperties; + + private ConsolidatedProperties(ElasticsearchProperties properties, + DeprecatedElasticsearchRestClientProperties deprecatedProperties) { + this.properties = properties; + this.deprecatedProperties = deprecatedProperties; + } + + private List getUris() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getUris() + : this.properties.getUris(); + } + + private String getUsername() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getUsername() + : this.properties.getUsername(); + } + + private String getPassword() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getPassword() + : this.properties.getPassword(); + } + + private Duration getConnectionTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getConnectionTimeout() + : this.properties.getConnectionTimeout(); + } + + private Duration getSocketTimeout() { + return this.deprecatedProperties.isCustomized() ? this.deprecatedProperties.getReadTimeout() + : this.properties.getSocketTimeout(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java index 649246d42bb..322c2344605 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -17,88 +17,24 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; + +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Configuration properties for Elasticsearch REST clients. + * Configuration properties specific to Elasticsearch's {@link RestClient} and + * {@link RestHighLevelClient}. * * @author Brian Clozel * @since 2.1.0 */ -@ConfigurationProperties(prefix = "spring.elasticsearch.rest") +@ConfigurationProperties(prefix = "spring.elasticsearch.restclient") public class ElasticsearchRestClientProperties { - /** - * Comma-separated list of the Elasticsearch instances to use. - */ - private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); - - /** - * Credentials username. - */ - private String username; - - /** - * Credentials password. - */ - private String password; - - /** - * Connection timeout. - */ - private Duration connectionTimeout = Duration.ofSeconds(1); - - /** - * Read timeout. - */ - private Duration readTimeout = Duration.ofSeconds(30); - private final Sniffer sniffer = new Sniffer(); - public List getUris() { - return this.uris; - } - - public void setUris(List uris) { - this.uris = uris; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getReadTimeout() { - return this.readTimeout; - } - - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } - public Sniffer getSniffer() { return this.sniffer; } 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 74146977bd9..cd89cd21d0c 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 @@ -860,6 +860,12 @@ "http://localhost:9200" ] }, + { + "name": "spring.elasticsearch.uris", + "defaultValue": [ + "http://localhost:9200" + ] + }, { "name": "spring.flyway.connect-retries-interval", "defaultValue": 120 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java index 55a09707a45..6a651b85a5d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -52,11 +52,10 @@ class ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests { @Test void restClientCanQueryElasticsearchNode() { - this.contextRunner.withPropertyValues( - "spring.data.elasticsearch.client.reactive.endpoints=" + elasticsearch.getHost() + ":" - + elasticsearch.getFirstMappedPort(), - "spring.data.elasticsearch.client.reactive.connection-timeout=120s", - "spring.data.elasticsearch.client.reactive.socket-timeout=120s").run((context) -> { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), + "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") + .run((context) -> { ReactiveElasticsearchClient client = context.getBean(ReactiveElasticsearchClient.class); Map source = new HashMap<>(); source.put("a", "alpha"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java index 2da45b8a9ce..f6180bafee9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java @@ -16,12 +16,20 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; import java.util.List; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -50,8 +58,14 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { @Test void configureShouldCreateDefaultBeans() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientConfiguration.class) - .hasSingleBean(ReactiveElasticsearchClient.class)); + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ClientConfiguration.class) + .hasSingleBean(ReactiveElasticsearchClient.class); + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9200); + }); } @Test @@ -68,6 +82,7 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } @Test + @Deprecated void whenEndpointIsCustomizedThenClientConfigurationHasCustomEndpoint() { this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876") .run((context) -> { @@ -79,6 +94,7 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } @Test + @Deprecated void whenMultipleEndpointsAreConfiguredThenClientConfigurationHasMultipleEndpoints() { this.contextRunner .withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876,localhost:8765") @@ -93,6 +109,122 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } @Test + void whenUriIsCustomizedThenClientConfigurationHasCustomEndpoint() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://localhost:9876").run((context) -> { + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + }); + } + + @Test + void whenUriHasHttpsSchemeThenClientConfigurationUsesSsl() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=https://localhost:9876").run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + List endpoints = clientConfiguration.getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(clientConfiguration.useSsl()).isTrue(); + }); + } + + @Test + void whenMultipleUrisAreConfiguredThenClientConfigurationHasMultipleEndpoints() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://localhost:9876,http://localhost:8765") + .run((context) -> { + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(2); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(endpoints.get(1).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(1).getPort()).isEqualTo(8765); + }); + } + + @Test + void whenMultipleUrisHaveHttpsSchemeThenClientConfigurationUsesSsl() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=https://localhost:9876,https://localhost:8765") + .run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + List endpoints = clientConfiguration.getEndpoints(); + assertThat(endpoints).hasSize(2); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(endpoints.get(1).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(1).getPort()).isEqualTo(8765); + assertThat(clientConfiguration.useSsl()).isTrue(); + }); + } + + @Test + void whenMultipleUrisHaveVaryingSchemesThenRunFails() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=https://localhost:9876,http://localhost:8765") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage("Configured Elasticsearch URIs have varying schemes"); + }); + } + + @Test + void whenUriHasUsernameOnlyThenDefaultAuthorizationHeaderHasUsernameAndEmptyPassword() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://user@localhost:9200").run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + assertThat(clientConfiguration.getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly( + "Basic " + Base64.getEncoder().encodeToString("user:".getBytes(StandardCharsets.UTF_8))); + }); + } + + @Test + void whenUriHasUsernameAndPasswordThenDefaultAuthorizationHeaderHasUsernameAndPassword() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9200") + .run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + assertThat(clientConfiguration.getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) + .containsExactly("Basic " + Base64.getEncoder() + .encodeToString("user:secret".getBytes(StandardCharsets.UTF_8))); + }); + } + + @Test + void whenMultipleUrisHaveVaryingUserInfosThenRunFails() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9876,http://localhost:8765") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage("Configured Elasticsearch URIs have varying user infos"); + }); + } + + @Test + void whenUriUserInfoMatchesUsernameAndPasswordPropertiesThenDefaultAuthorizationHeaderIsConfigured() { + this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9876", + "spring.elasticsearch.username=user", "spring.elasticsearch.password=secret").run((context) -> { + ClientConfiguration clientConfiguration = context.getBean(ClientConfiguration.class); + assertThat(clientConfiguration.getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) + .containsExactly("Basic " + Base64.getEncoder() + .encodeToString("user:secret".getBytes(StandardCharsets.UTF_8))); + }); + } + + @Test + void whenUriUserInfoAndUsernameAndPasswordPropertiesDoNotMatchThenRunFails() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=http://user:secret@localhost:9876", + "spring.elasticsearch.username=alice", "spring.elasticsearch.password=confidential") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage("Credentials from URI user info do not match those from " + + "spring.elasticsearch.username and spring.elasticsearch.password"); + }); + } + + @Test + @Deprecated void whenConfiguredToUseSslThenClientConfigurationUsesSsl() { this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.use-ssl=true") .run((context) -> assertThat(context.getBean(ClientConfiguration.class).useSsl()).isTrue()); @@ -101,50 +233,46 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { @Test void whenSocketTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) - .isEqualTo(Duration.ofSeconds(5))); + .isEqualTo(Duration.ofSeconds(30))); } @Test void whenConnectionTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) - .isEqualTo(Duration.ofSeconds(10))); + .isEqualTo(Duration.ofSeconds(1))); } - @Test - void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.socket-timeout=2s") + @ParameterizedPropertyPrefixTest + void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout(String prefix) { + this.contextRunner.withPropertyValues(prefix + "socket-timeout=2s") .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) .isEqualTo(Duration.ofSeconds(2))); } - @Test - void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.connection-timeout=2s") + @ParameterizedPropertyPrefixTest + void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout(String prefix) { + this.contextRunner.withPropertyValues(prefix + "connection-timeout=2s") .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) .isEqualTo(Duration.ofSeconds(2))); } - @Test - void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader() { - this.contextRunner - .withPropertyValues("spring.data.elasticsearch.client.reactive.username=alice", - "spring.data.elasticsearch.client.reactive.password=secret") + @ParameterizedPropertyPrefixTest + void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader(String prefix) { + this.contextRunner.withPropertyValues(prefix + "username=alice", prefix + "password=secret") .run((context) -> assertThat( context.getBean(ClientConfiguration.class).getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) .containsExactly("Basic YWxpY2U6c2VjcmV0")); } - @Test - void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.max-in-memory-size=1MB") - .run((context) -> { - WebClient client = configureWebClient( - context.getBean(ClientConfiguration.class).getClientConfigurers()); - assertThat(client).extracting("exchangeFunction").extracting("strategies") - .extracting("codecConfigurer").extracting("defaultCodecs") - .asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class)) - .extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024); - }); + @ParameterizedTest + @ValueSource(strings = { "spring.elasticsearch.webclient.", "spring.data.elasticsearch.client.reactive." }) + void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize(String prefix) { + this.contextRunner.withPropertyValues(prefix + "max-in-memory-size=1MB").run((context) -> { + WebClient client = configureWebClient(context.getBean(ClientConfiguration.class).getClientConfigurers()); + assertThat(client).extracting("exchangeFunction").extracting("strategies").extracting("codecConfigurer") + .extracting("defaultCodecs").asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class)) + .extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024); + }); } private WebClient configureWebClient(List> callbacks) { @@ -175,4 +303,12 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests { } + @ParameterizedTest + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(strings = { "spring.data.elasticsearch.client.reactive.", "spring.elasticsearch." }) + static @interface ParameterizedPropertyPrefixTest { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java index 6165778f71a..49dbdb7e289 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -55,7 +55,8 @@ class ElasticsearchRestClientAutoConfigurationIntegrationTests { @Test void restClientCanQueryElasticsearchNode() { this.contextRunner - .withPropertyValues("spring.elasticsearch.rest.uris=http://" + elasticsearch.getHttpHostAddress()) + .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), + "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") .run((context) -> { RestHighLevelClient client = context.getBean(RestHighLevelClient.class); Map source = new HashMap<>(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java index bb7b457beb1..fac35367e0b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.elasticsearch; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Map; @@ -32,6 +36,8 @@ import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.sniff.Sniffer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -105,7 +111,7 @@ class ElasticsearchRestClientAutoConfigurationTests { } @Test - void configureWithCustomTimeouts() { + void configureWithLegacyCustomTimeouts() { this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s", "spring.elasticsearch.rest.read-timeout=1m").run((context) -> { assertThat(context).hasSingleBean(RestHighLevelClient.class); @@ -114,6 +120,16 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } + @Test + void configureWithCustomTimeouts() { + this.contextRunner.withPropertyValues("spring.elasticsearch.connection-timeout=15s", + "spring.elasticsearch.socket-timeout=1m").run((context) -> { + assertThat(context).hasSingleBean(RestHighLevelClient.class); + RestHighLevelClient restClient = context.getBean(RestHighLevelClient.class); + assertTimeouts(restClient, Duration.ofSeconds(15), Duration.ofMinutes(1)); + }); + } + private static void assertTimeouts(RestHighLevelClient restClient, Duration connectTimeout, Duration readTimeout) { assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.socketTimeout") .isEqualTo(Math.toIntExact(readTimeout.toMillis())); @@ -121,50 +137,51 @@ class ElasticsearchRestClientAutoConfigurationTests { .isEqualTo(Math.toIntExact(connectTimeout.toMillis())); } - @Test - void configureUriWithUsernameOnly() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user@localhost:9200") - .run((context) -> { - RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); - assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) - .containsExactly("http://localhost:9200"); - assertThat(client).extracting("client") - .extracting("credentialsProvider", - InstanceOfAssertFactories.type(CredentialsProvider.class)) - .satisfies((credentialsProvider) -> { - Credentials credentials = credentialsProvider - .getCredentials(new AuthScope("localhost", 9200)); - assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); - assertThat(credentials.getPassword()).isNull(); - }); - }); + @ParameterizedPropertyPrefixTest + void configureUriWithNoScheme(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=localhost:9876").run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9876"); + }); } - @Test - void configureUriWithUsernameAndEmptyPassword() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user:@localhost:9200") - .run((context) -> { - RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); - assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) - .containsExactly("http://localhost:9200"); - assertThat(client).extracting("client") - .extracting("credentialsProvider", - InstanceOfAssertFactories.type(CredentialsProvider.class)) - .satisfies((credentialsProvider) -> { - Credentials credentials = credentialsProvider - .getCredentials(new AuthScope("localhost", 9200)); - assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); - assertThat(credentials.getPassword()).isEmpty(); - }); - }); + @ParameterizedPropertyPrefixTest + void configureUriWithUsernameOnly(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=http://user@localhost:9200").run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider.getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isNull(); + }); + }); } - @Test - void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet() { - this.contextRunner - .withPropertyValues("spring.elasticsearch.rest.uris=http://user:password@localhost:9200,localhost:9201", - "spring.elasticsearch.rest.username=admin", "spring.elasticsearch.rest.password=admin") - .run((context) -> { + @ParameterizedPropertyPrefixTest + void configureUriWithUsernameAndEmptyPassword(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=http://user:@localhost:9200").run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider.getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isEmpty(); + }); + }); + } + + @ParameterizedPropertyPrefixTest + void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet(String prefix) { + this.contextRunner.withPropertyValues(prefix + "uris=http://user:password@localhost:9200,localhost:9201", + prefix + "username=admin", prefix + "password=admin").run((context) -> { RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9200", "http://localhost:9201"); @@ -203,10 +220,10 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } - @Test - void configureWithCustomSnifferSettings() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.sniffer.interval=180s", - "spring.elasticsearch.rest.sniffer.delay-after-failure=30s").run((context) -> { + @ParameterizedSnifferPropertyPrefixTest + void configureWithCustomSnifferSettings(String prefix) { + this.contextRunner.withPropertyValues(prefix + "interval=180s", prefix + "delay-after-failure=30s") + .run((context) -> { assertThat(context).hasSingleBean(Sniffer.class); Sniffer sniffer = context.getBean(Sniffer.class); assertThat(sniffer).hasFieldOrPropertyWithValue("sniffIntervalMillis", @@ -279,4 +296,20 @@ class ElasticsearchRestClientAutoConfigurationTests { } + @ParameterizedTest + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(strings = { "spring.elasticsearch.rest.", "spring.elasticsearch." }) + static @interface ParameterizedPropertyPrefixTest { + + } + + @ParameterizedTest + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(strings = { "spring.elasticsearch.rest.sniffer.", "spring.elasticsearch.restclient.sniffer." }) + static @interface ParameterizedSnifferPropertyPrefixTest { + + } + } 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 3719c3de8f9..ff3a4d7c40d 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 @@ -261,7 +261,7 @@ If you add your own `@Bean` of type `SolrClient`, it replaces the default. [[data.nosql.elasticsearch]] === Elasticsearch https://www.elastic.co/products/elasticsearch[Elasticsearch] is an open source, distributed, RESTful search and analytics engine. -Spring Boot offers basic auto-configuration for Elasticsearch. +Spring Boot offers basic auto-configuration for Elasticsearch clients. Spring Boot supports several clients: @@ -276,34 +276,36 @@ Spring Boot provides a dedicated "`Starter`", `spring-boot-starter-data-elastics ==== Connecting to Elasticsearch using REST clients Elasticsearch ships https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html[two different REST clients] that you can use to query a cluster: the "Low Level" client and the "High Level" client. Spring Boot provides support for the "High Level" client, which ships with `org.elasticsearch.client:elasticsearch-rest-high-level-client`. - -If you have this dependency on the classpath, Spring Boot will auto-configure and register a `RestHighLevelClient` bean that by default targets `http://localhost:9200`. -You can further tune how `RestHighLevelClient` is configured, as shown in the following example: +Additionally, Spring Boot provides support for a reactive client, based on Spring Framework's `WebClient`, that ships with `org.springframework.data:spring-data-elasticsearch`. +By default, the clients will target `http://localhost:9200`. +You can use `spring.elasticsearch.*` properties to further tune how the clients are configured, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- spring: elasticsearch: - rest: - uris: "https://search.example.com:9200" - read-timeout: "10s" - username: "user" - password: "secret" + uris: "https://search.example.com:9200" + socket-timeout: "10s" + username: "user" + password: "secret" ---- -You can also register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. -To take full control over the registration, define a `RestClientBuilder` bean. +[[data.nosql.elasticsearch.connecting-using-rest.restclient]] +===== Connecting to Elasticsearch using RestHighLevelClient +If you have `elasticsearch-rest-high-level-client` on the classpath, Spring Boot will auto-configure and register a `RestHighLevelClient` bean. +In addition to the properties described previously, to fine-tune the `RestHighLevelClient`, you can register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. +To take full control over its registration, define a `RestClientBuilder` bean. TIP: If your application needs access to a "Low Level" `RestClient`, you can get it by calling `client.getLowLevelClient()` on the auto-configured `RestHighLevelClient`. -Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them to the `RestHighLevelClient` bean. +Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them on the `RestHighLevelClient` bean. You can further tune how `Sniffer` is configured, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- spring: elasticsearch: - rest: + restclient: sniffer: interval: 10m delay-after-failure: 30s @@ -311,31 +313,23 @@ You can further tune how `Sniffer` is configured, as shown in the following exam -[[data.nosql.elasticsearch.connecting-using-reactive-rest]] -==== Connecting to Elasticsearch using Reactive REST clients +[[data.nosql.elasticsearch.connecting-using-rest.webclient]] +===== Connecting to Elasticsearch using ReactiveElasticsearchClient {spring-data-elasticsearch}[Spring Data Elasticsearch] ships `ReactiveElasticsearchClient` for querying Elasticsearch instances in a reactive fashion. It is built on top of WebFlux's `WebClient`, so both `spring-boot-starter-elasticsearch` and `spring-boot-starter-webflux` dependencies are useful to enable this support. -By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient` -bean that targets `http://localhost:9200`. -You can further tune how it is configured, as shown in the following example: +By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient`. +In addition to the properties described previously, the `spring.elasticsearch.webclient.*` properties can be used to configure reactive-specific settings, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- spring: - data: - elasticsearch: - client: - reactive: - endpoints: "search.example.com:9200" - use-ssl: true - socket-timeout: "10s" - username: "user" - password: "secret" + elasticsearch: + webclient: + max-in-memory-size: 1MB ---- -If the configuration properties are not enough and you'd like to fully control the client -configuration, you can register a custom `ClientConfiguration` bean. +If the `spring.elasticsearch.*` and `spring.elasticsearch.webclient.*` configuration properties are not enough and you'd like to fully control the client configuration, you can register a custom `ClientConfiguration` bean. @@ -352,7 +346,7 @@ as shown in the following example: include::{docs-java}/features/nosql/elasticsearch/connectingusingspringdata/MyBean.java[] ---- -In the presence of `spring-data-elasticsearch` and the required dependencies for using a `WebClient` (typically `spring-boot-starter-webflux`), Spring Boot can also auto-configure a <> and a `ReactiveElasticsearchTemplate` as beans. +In the presence of `spring-data-elasticsearch` and the required dependencies for using a `WebClient` (typically `spring-boot-starter-webflux`), Spring Boot can also auto-configure a <> and a `ReactiveElasticsearchTemplate` as beans. They are the reactive equivalent of the other REST clients. diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 7a6938e9f24..41eb4bea3d4 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -54,4 +54,7 @@ + + +