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
pull/28071/head
Andy Wilkinson 3 years ago
parent dd366af849
commit e2a355f003

@ -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<String> 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<String> getEndpoints() {
return this.endpoints;
}
public void setEndpoints(List<String> 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;
}
}

@ -16,11 +16,18 @@
package org.springframework.boot.autoconfigure.data.elasticsearch; 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 reactor.netty.http.client.HttpClient;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean; 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.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients; import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients;
import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients.WebClientConfigurationCallback; import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients.WebClientConfigurationCallback;
import org.springframework.util.Assert;
import org.springframework.util.unit.DataSize; import org.springframework.util.unit.DataSize;
import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
@ -40,30 +48,39 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Brian Clozel * @author Brian Clozel
* @since 2.2.0 * @since 2.2.0
*/ */
@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ReactiveRestClients.class, WebClient.class, HttpClient.class }) @ConditionalOnClass({ ReactiveRestClients.class, WebClient.class, HttpClient.class })
@EnableConfigurationProperties(ReactiveElasticsearchRestClientProperties.class) @EnableConfigurationProperties({ ElasticsearchProperties.class, ReactiveElasticsearchRestClientProperties.class,
DeprecatedReactiveElasticsearchRestClientProperties.class })
public class ReactiveElasticsearchRestClientAutoConfiguration { public class ReactiveElasticsearchRestClientAutoConfiguration {
private final ConsolidatedProperties properties;
ReactiveElasticsearchRestClientAutoConfiguration(ElasticsearchProperties properties,
ReactiveElasticsearchRestClientProperties restClientProperties,
DeprecatedReactiveElasticsearchRestClientProperties reactiveProperties) {
this.properties = new ConsolidatedProperties(properties, restClientProperties, reactiveProperties);
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public ClientConfiguration clientConfiguration(ReactiveElasticsearchRestClientProperties properties) { public ClientConfiguration clientConfiguration() {
ClientConfiguration.MaybeSecureClientConfigurationBuilder builder = ClientConfiguration.builder() 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(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties.isUseSsl()).whenTrue().toCall(builder::usingSsl); map.from(this.properties.isUseSsl()).whenTrue().toCall(builder::usingSsl);
map.from(properties.getUsername()).whenHasText() map.from(this.properties.getCredentials())
.to((username) -> builder.withBasicAuth(username, properties.getPassword())); .to((credentials) -> builder.withBasicAuth(credentials.getUsername(), credentials.getPassword()));
map.from(properties.getConnectionTimeout()).to(builder::withConnectTimeout); map.from(this.properties.getConnectionTimeout()).to(builder::withConnectTimeout);
map.from(properties.getSocketTimeout()).to(builder::withSocketTimeout); map.from(this.properties.getSocketTimeout()).to(builder::withSocketTimeout);
configureExchangeStrategies(map, builder, properties); configureExchangeStrategies(map, builder);
return builder.build(); return builder.build();
} }
private void configureExchangeStrategies(PropertyMapper map, private void configureExchangeStrategies(PropertyMapper map,
ClientConfiguration.TerminalClientConfigurationBuilder builder, ClientConfiguration.TerminalClientConfigurationBuilder builder) {
ReactiveElasticsearchRestClientProperties properties) { map.from(this.properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> {
map.from(properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> {
builder.withClientConfigurer(WebClientConfigurationCallback.from((webClient) -> { builder.withClientConfigurer(WebClientConfigurationCallback.from((webClient) -> {
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize)).build(); .codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize)).build();
@ -78,4 +95,166 @@ public class ReactiveElasticsearchRestClientAutoConfiguration {
return ReactiveRestClients.create(clientConfiguration); return ReactiveRestClients.create(clientConfiguration);
} }
private static final class ConsolidatedProperties {
private final ElasticsearchProperties properties;
private final ReactiveElasticsearchRestClientProperties restClientProperties;
private final DeprecatedReactiveElasticsearchRestClientProperties deprecatedProperties;
private final List<URI> 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<String> 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<String> 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<String> uris) {
Set<String> 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;
}
}
}
} }

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; 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.ConfigurationProperties;
import org.springframework.util.unit.DataSize; import org.springframework.util.unit.DataSize;
@ -30,93 +25,15 @@ import org.springframework.util.unit.DataSize;
* @author Brian Clozel * @author Brian Clozel
* @since 2.2.0 * @since 2.2.0
*/ */
@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") @ConfigurationProperties(prefix = "spring.elasticsearch.webclient")
public class ReactiveElasticsearchRestClientProperties { public class ReactiveElasticsearchRestClientProperties {
/**
* Comma-separated list of the Elasticsearch endpoints to connect to.
*/
private List<String> 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 * Limit on the number of bytes that can be buffered whenever the input stream needs
* to be aggregated. * to be aggregated.
*/ */
private DataSize maxInMemorySize; private DataSize maxInMemorySize;
public List<String> getEndpoints() {
return this.endpoints;
}
public void setEndpoints(List<String> 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() { public DataSize getMaxInMemorySize() {
return this.maxInMemorySize; return this.maxInMemorySize;
} }

@ -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<String> 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<String> getUris() {
return this.uris;
}
public void setUris(List<String> 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;
}
}
}

@ -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<String> 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<String> getUris() {
return this.uris;
}
public void setUris(List<String> 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;
}
}

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * @author Stephane Nicoll
* @since 2.1.0 * @since 2.1.0
*/ */
@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestHighLevelClient.class) @ConditionalOnClass(RestHighLevelClient.class)
@ConditionalOnMissingBean(RestClient.class) @ConditionalOnMissingBean(RestClient.class)
@EnableConfigurationProperties(ElasticsearchRestClientProperties.class) @EnableConfigurationProperties({ ElasticsearchProperties.class, ElasticsearchRestClientProperties.class,
DeprecatedElasticsearchRestClientProperties.class })
@Import({ RestClientBuilderConfiguration.class, RestHighLevelClientConfiguration.class, @Import({ RestClientBuilderConfiguration.class, RestHighLevelClientConfiguration.class,
RestClientSnifferConfiguration.class }) RestClientSnifferConfiguration.class })
public class ElasticsearchRestClientAutoConfiguration { public class ElasticsearchRestClientAutoConfiguration {

@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.elasticsearch;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import org.apache.http.HttpHost; import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope; import org.apache.http.auth.AuthScope;
@ -53,15 +54,23 @@ class ElasticsearchRestClientConfigurations {
@ConditionalOnMissingBean(RestClientBuilder.class) @ConditionalOnMissingBean(RestClientBuilder.class)
static class RestClientBuilderConfiguration { static class RestClientBuilderConfiguration {
private final ConsolidatedProperties properties;
@SuppressWarnings("deprecation")
RestClientBuilderConfiguration(ElasticsearchProperties properties,
DeprecatedElasticsearchRestClientProperties deprecatedProperties) {
this.properties = new ConsolidatedProperties(properties, deprecatedProperties);
}
@Bean @Bean
RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { RestClientBuilderCustomizer defaultRestClientBuilderCustomizer() {
return new DefaultRestClientBuilderCustomizer(properties); return new DefaultRestClientBuilderCustomizer(this.properties);
} }
@Bean @Bean
RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, RestClientBuilder elasticsearchRestClientBuilder(
ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) { ObjectProvider<RestClientBuilderCustomizer> 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); RestClientBuilder builder = RestClient.builder(hosts);
builder.setHttpClientConfigCallback((httpClientBuilder) -> { builder.setHttpClientConfigCallback((httpClientBuilder) -> {
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder));
@ -117,13 +126,18 @@ class ElasticsearchRestClientConfigurations {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties) { @SuppressWarnings("deprecation")
Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties,
DeprecatedElasticsearchRestClientProperties deprecatedProperties) {
SnifferBuilder builder = Sniffer.builder(client.getLowLevelClient()); SnifferBuilder builder = Sniffer.builder(client.getLowLevelClient());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties.getSniffer().getInterval()).asInt(Duration::toMillis) Duration interval = deprecatedProperties.isCustomized() ? deprecatedProperties.getSniffer().getInterval()
.to(builder::setSniffIntervalMillis); : properties.getSniffer().getInterval();
map.from(properties.getSniffer().getDelayAfterFailure()).asInt(Duration::toMillis) map.from(interval).asInt(Duration::toMillis).to(builder::setSniffIntervalMillis);
.to(builder::setSniffAfterFailureDelayMillis); Duration delayAfterFailure = deprecatedProperties.isCustomized()
? deprecatedProperties.getSniffer().getDelayAfterFailure()
: properties.getSniffer().getDelayAfterFailure();
map.from(delayAfterFailure).asInt(Duration::toMillis).to(builder::setSniffAfterFailureDelayMillis);
return builder.build(); return builder.build();
} }
@ -133,9 +147,9 @@ class ElasticsearchRestClientConfigurations {
private static final PropertyMapper map = PropertyMapper.get(); private static final PropertyMapper map = PropertyMapper.get();
private final ElasticsearchRestClientProperties properties; private final ConsolidatedProperties properties;
DefaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { DefaultRestClientBuilderCustomizer(ConsolidatedProperties properties) {
this.properties = properties; this.properties = properties;
} }
@ -152,7 +166,7 @@ class ElasticsearchRestClientConfigurations {
public void customize(RequestConfig.Builder builder) { public void customize(RequestConfig.Builder builder) {
map.from(this.properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) map.from(this.properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis)
.to(builder::setConnectTimeout); .to(builder::setConnectTimeout);
map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) map.from(this.properties::getSocketTimeout).whenNonNull().asInt(Duration::toMillis)
.to(builder::setSocketTimeout); .to(builder::setSocketTimeout);
} }
@ -160,7 +174,7 @@ class ElasticsearchRestClientConfigurations {
private static class PropertiesCredentialsProvider extends BasicCredentialsProvider { private static class PropertiesCredentialsProvider extends BasicCredentialsProvider {
PropertiesCredentialsProvider(ElasticsearchRestClientProperties properties) { PropertiesCredentialsProvider(ConsolidatedProperties properties) {
if (StringUtils.hasText(properties.getUsername())) { if (StringUtils.hasText(properties.getUsername())) {
Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(),
properties.getPassword()); 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<String> 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();
}
}
} }

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,88 +17,24 @@
package org.springframework.boot.autoconfigure.elasticsearch; package org.springframework.boot.autoconfigure.elasticsearch;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections; import org.elasticsearch.client.RestClient;
import java.util.List; import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties; 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 * @author Brian Clozel
* @since 2.1.0 * @since 2.1.0
*/ */
@ConfigurationProperties(prefix = "spring.elasticsearch.rest") @ConfigurationProperties(prefix = "spring.elasticsearch.restclient")
public class ElasticsearchRestClientProperties { public class ElasticsearchRestClientProperties {
/**
* Comma-separated list of the Elasticsearch instances to use.
*/
private List<String> 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 final Sniffer sniffer = new Sniffer();
public List<String> getUris() {
return this.uris;
}
public void setUris(List<String> 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() { public Sniffer getSniffer() {
return this.sniffer; return this.sniffer;
} }

@ -860,6 +860,12 @@
"http://localhost:9200" "http://localhost:9200"
] ]
}, },
{
"name": "spring.elasticsearch.uris",
"defaultValue": [
"http://localhost:9200"
]
},
{ {
"name": "spring.flyway.connect-retries-interval", "name": "spring.flyway.connect-retries-interval",
"defaultValue": 120 "defaultValue": 120

@ -52,11 +52,10 @@ class ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests {
@Test @Test
void restClientCanQueryElasticsearchNode() { void restClientCanQueryElasticsearchNode() {
this.contextRunner.withPropertyValues( this.contextRunner
"spring.data.elasticsearch.client.reactive.endpoints=" + elasticsearch.getHost() + ":" .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(),
+ elasticsearch.getFirstMappedPort(), "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s")
"spring.data.elasticsearch.client.reactive.connection-timeout=120s", .run((context) -> {
"spring.data.elasticsearch.client.reactive.socket-timeout=120s").run((context) -> {
ReactiveElasticsearchClient client = context.getBean(ReactiveElasticsearchClient.class); ReactiveElasticsearchClient client = context.getBean(ReactiveElasticsearchClient.class);
Map<String, String> source = new HashMap<>(); Map<String, String> source = new HashMap<>();
source.put("a", "alpha"); source.put("a", "alpha");

@ -16,12 +16,20 @@
package org.springframework.boot.autoconfigure.data.elasticsearch; 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.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Base64;
import java.util.List; import java.util.List;
import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test; 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.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@ -50,8 +58,14 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests {
@Test @Test
void configureShouldCreateDefaultBeans() { void configureShouldCreateDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientConfiguration.class) this.contextRunner.run((context) -> {
.hasSingleBean(ReactiveElasticsearchClient.class)); assertThat(context).hasSingleBean(ClientConfiguration.class)
.hasSingleBean(ReactiveElasticsearchClient.class);
List<InetSocketAddress> 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 @Test
@ -68,6 +82,7 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests {
} }
@Test @Test
@Deprecated
void whenEndpointIsCustomizedThenClientConfigurationHasCustomEndpoint() { void whenEndpointIsCustomizedThenClientConfigurationHasCustomEndpoint() {
this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876") this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876")
.run((context) -> { .run((context) -> {
@ -79,6 +94,7 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests {
} }
@Test @Test
@Deprecated
void whenMultipleEndpointsAreConfiguredThenClientConfigurationHasMultipleEndpoints() { void whenMultipleEndpointsAreConfiguredThenClientConfigurationHasMultipleEndpoints() {
this.contextRunner this.contextRunner
.withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876,localhost:8765") .withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876,localhost:8765")
@ -93,6 +109,122 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests {
} }
@Test @Test
void whenUriIsCustomizedThenClientConfigurationHasCustomEndpoint() {
this.contextRunner.withPropertyValues("spring.elasticsearch.uris=http://localhost:9876").run((context) -> {
List<InetSocketAddress> 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<InetSocketAddress> 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<InetSocketAddress> 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<InetSocketAddress> 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() { void whenConfiguredToUseSslThenClientConfigurationUsesSsl() {
this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.use-ssl=true") this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.use-ssl=true")
.run((context) -> assertThat(context.getBean(ClientConfiguration.class).useSsl()).isTrue()); .run((context) -> assertThat(context.getBean(ClientConfiguration.class).useSsl()).isTrue());
@ -101,50 +233,46 @@ class ReactiveElasticsearchRestClientAutoConfigurationTests {
@Test @Test
void whenSocketTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { void whenSocketTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() {
this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout())
.isEqualTo(Duration.ofSeconds(5))); .isEqualTo(Duration.ofSeconds(30)));
} }
@Test @Test
void whenConnectionTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { void whenConnectionTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() {
this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout())
.isEqualTo(Duration.ofSeconds(10))); .isEqualTo(Duration.ofSeconds(1)));
} }
@Test @ParameterizedPropertyPrefixTest
void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout() { void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout(String prefix) {
this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.socket-timeout=2s") this.contextRunner.withPropertyValues(prefix + "socket-timeout=2s")
.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout())
.isEqualTo(Duration.ofSeconds(2))); .isEqualTo(Duration.ofSeconds(2)));
} }
@Test @ParameterizedPropertyPrefixTest
void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout() { void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout(String prefix) {
this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.connection-timeout=2s") this.contextRunner.withPropertyValues(prefix + "connection-timeout=2s")
.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout())
.isEqualTo(Duration.ofSeconds(2))); .isEqualTo(Duration.ofSeconds(2)));
} }
@Test @ParameterizedPropertyPrefixTest
void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader() { void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader(String prefix) {
this.contextRunner this.contextRunner.withPropertyValues(prefix + "username=alice", prefix + "password=secret")
.withPropertyValues("spring.data.elasticsearch.client.reactive.username=alice",
"spring.data.elasticsearch.client.reactive.password=secret")
.run((context) -> assertThat( .run((context) -> assertThat(
context.getBean(ClientConfiguration.class).getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) context.getBean(ClientConfiguration.class).getDefaultHeaders().get(HttpHeaders.AUTHORIZATION))
.containsExactly("Basic YWxpY2U6c2VjcmV0")); .containsExactly("Basic YWxpY2U6c2VjcmV0"));
} }
@Test @ParameterizedTest
void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize() { @ValueSource(strings = { "spring.elasticsearch.webclient.", "spring.data.elasticsearch.client.reactive." })
this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.max-in-memory-size=1MB") void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize(String prefix) {
.run((context) -> { this.contextRunner.withPropertyValues(prefix + "max-in-memory-size=1MB").run((context) -> {
WebClient client = configureWebClient( WebClient client = configureWebClient(context.getBean(ClientConfiguration.class).getClientConfigurers());
context.getBean(ClientConfiguration.class).getClientConfigurers()); assertThat(client).extracting("exchangeFunction").extracting("strategies").extracting("codecConfigurer")
assertThat(client).extracting("exchangeFunction").extracting("strategies") .extracting("defaultCodecs").asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class))
.extracting("codecConfigurer").extracting("defaultCodecs") .extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024);
.asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class)) });
.extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024);
});
} }
private WebClient configureWebClient(List<ClientConfigurationCallback<?>> callbacks) { private WebClient configureWebClient(List<ClientConfigurationCallback<?>> 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 {
}
} }

@ -55,7 +55,8 @@ class ElasticsearchRestClientAutoConfigurationIntegrationTests {
@Test @Test
void restClientCanQueryElasticsearchNode() { void restClientCanQueryElasticsearchNode() {
this.contextRunner 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) -> { .run((context) -> {
RestHighLevelClient client = context.getBean(RestHighLevelClient.class); RestHighLevelClient client = context.getBean(RestHighLevelClient.class);
Map<String, String> source = new HashMap<>(); Map<String, String> source = new HashMap<>();

@ -16,6 +16,10 @@
package org.springframework.boot.autoconfigure.elasticsearch; 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.time.Duration;
import java.util.Map; import java.util.Map;
@ -32,6 +36,8 @@ import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.sniff.Sniffer; import org.elasticsearch.client.sniff.Sniffer;
import org.junit.jupiter.api.Test; 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.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
@ -105,7 +111,7 @@ class ElasticsearchRestClientAutoConfigurationTests {
} }
@Test @Test
void configureWithCustomTimeouts() { void configureWithLegacyCustomTimeouts() {
this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s", this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s",
"spring.elasticsearch.rest.read-timeout=1m").run((context) -> { "spring.elasticsearch.rest.read-timeout=1m").run((context) -> {
assertThat(context).hasSingleBean(RestHighLevelClient.class); 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) { private static void assertTimeouts(RestHighLevelClient restClient, Duration connectTimeout, Duration readTimeout) {
assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.socketTimeout") assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.socketTimeout")
.isEqualTo(Math.toIntExact(readTimeout.toMillis())); .isEqualTo(Math.toIntExact(readTimeout.toMillis()));
@ -121,50 +137,51 @@ class ElasticsearchRestClientAutoConfigurationTests {
.isEqualTo(Math.toIntExact(connectTimeout.toMillis())); .isEqualTo(Math.toIntExact(connectTimeout.toMillis()));
} }
@Test @ParameterizedPropertyPrefixTest
void configureUriWithUsernameOnly() { void configureUriWithNoScheme(String prefix) {
this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user@localhost:9200") this.contextRunner.withPropertyValues(prefix + "uris=localhost:9876").run((context) -> {
.run((context) -> { RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient();
RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString))
assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9876");
.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 @ParameterizedPropertyPrefixTest
void configureUriWithUsernameAndEmptyPassword() { void configureUriWithUsernameOnly(String prefix) {
this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user:@localhost:9200") this.contextRunner.withPropertyValues(prefix + "uris=http://user@localhost:9200").run((context) -> {
.run((context) -> { RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient();
RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString))
assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9200");
.containsExactly("http://localhost:9200"); assertThat(client).extracting("client")
assertThat(client).extracting("client") .extracting("credentialsProvider", InstanceOfAssertFactories.type(CredentialsProvider.class))
.extracting("credentialsProvider", .satisfies((credentialsProvider) -> {
InstanceOfAssertFactories.type(CredentialsProvider.class)) Credentials credentials = credentialsProvider.getCredentials(new AuthScope("localhost", 9200));
.satisfies((credentialsProvider) -> { assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user");
Credentials credentials = credentialsProvider assertThat(credentials.getPassword()).isNull();
.getCredentials(new AuthScope("localhost", 9200)); });
assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); });
assertThat(credentials.getPassword()).isEmpty();
});
});
} }
@Test @ParameterizedPropertyPrefixTest
void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet() { void configureUriWithUsernameAndEmptyPassword(String prefix) {
this.contextRunner this.contextRunner.withPropertyValues(prefix + "uris=http://user:@localhost:9200").run((context) -> {
.withPropertyValues("spring.elasticsearch.rest.uris=http://user:password@localhost:9200,localhost:9201", RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient();
"spring.elasticsearch.rest.username=admin", "spring.elasticsearch.rest.password=admin") assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString))
.run((context) -> { .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(); RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient();
assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString))
.containsExactly("http://localhost:9200", "http://localhost:9201"); .containsExactly("http://localhost:9200", "http://localhost:9201");
@ -203,10 +220,10 @@ class ElasticsearchRestClientAutoConfigurationTests {
}); });
} }
@Test @ParameterizedSnifferPropertyPrefixTest
void configureWithCustomSnifferSettings() { void configureWithCustomSnifferSettings(String prefix) {
this.contextRunner.withPropertyValues("spring.elasticsearch.rest.sniffer.interval=180s", this.contextRunner.withPropertyValues(prefix + "interval=180s", prefix + "delay-after-failure=30s")
"spring.elasticsearch.rest.sniffer.delay-after-failure=30s").run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(Sniffer.class); assertThat(context).hasSingleBean(Sniffer.class);
Sniffer sniffer = context.getBean(Sniffer.class); Sniffer sniffer = context.getBean(Sniffer.class);
assertThat(sniffer).hasFieldOrPropertyWithValue("sniffIntervalMillis", 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 {
}
} }

@ -261,7 +261,7 @@ If you add your own `@Bean` of type `SolrClient`, it replaces the default.
[[data.nosql.elasticsearch]] [[data.nosql.elasticsearch]]
=== Elasticsearch === Elasticsearch
https://www.elastic.co/products/elasticsearch[Elasticsearch] is an open source, distributed, RESTful search and analytics engine. 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: 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 ==== 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. 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`. Spring Boot provides support for the "High Level" client, which ships with `org.elasticsearch.client:elasticsearch-rest-high-level-client`.
Additionally, Spring Boot provides support for a reactive client, based on Spring Framework's `WebClient`, that ships with `org.springframework.data:spring-data-elasticsearch`.
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`. By default, the clients will target `http://localhost:9200`.
You can further tune how `RestHighLevelClient` is configured, as shown in the following example: 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] [source,yaml,indent=0,subs="verbatim",configprops,configblocks]
---- ----
spring: spring:
elasticsearch: elasticsearch:
rest: uris: "https://search.example.com:9200"
uris: "https://search.example.com:9200" socket-timeout: "10s"
read-timeout: "10s" username: "user"
username: "user" password: "secret"
password: "secret"
---- ----
You can also register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. [[data.nosql.elasticsearch.connecting-using-rest.restclient]]
To take full control over the registration, define a `RestClientBuilder` bean. ===== 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`. 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: You can further tune how `Sniffer` is configured, as shown in the following example:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks] [source,yaml,indent=0,subs="verbatim",configprops,configblocks]
---- ----
spring: spring:
elasticsearch: elasticsearch:
rest: restclient:
sniffer: sniffer:
interval: 10m interval: 10m
delay-after-failure: 30s 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]] [[data.nosql.elasticsearch.connecting-using-rest.webclient]]
==== Connecting to Elasticsearch using Reactive REST clients ===== Connecting to Elasticsearch using ReactiveElasticsearchClient
{spring-data-elasticsearch}[Spring Data Elasticsearch] ships `ReactiveElasticsearchClient` for querying Elasticsearch instances in a reactive fashion. {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. 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` By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient`.
bean that targets `http://localhost:9200`. 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:
You can further tune how it is configured, as shown in the following example:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks] [source,yaml,indent=0,subs="verbatim",configprops,configblocks]
---- ----
spring: spring:
data: elasticsearch:
elasticsearch: webclient:
client: max-in-memory-size: 1MB
reactive:
endpoints: "search.example.com:9200"
use-ssl: true
socket-timeout: "10s"
username: "user"
password: "secret"
---- ----
If the configuration properties are not enough and you'd like to fully control the client 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.
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[] 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 <<features#data.nosql.elasticsearch.connecting-using-reactive-rest,ReactiveElasticsearchClient>> 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 <<features#data.nosql.elasticsearch.connecting-using-rest.webclient,ReactiveElasticsearchClient>> and a `ReactiveElasticsearchTemplate` as beans.
They are the reactive equivalent of the other REST clients. They are the reactive equivalent of the other REST clients.

@ -54,4 +54,7 @@
<suppress files="BsdDomainSocket" checks="FinalClass" message="SockaddrUn" /> <suppress files="BsdDomainSocket" checks="FinalClass" message="SockaddrUn" />
<suppress files="StringSequence" checks="SpringMethodVisibility" message="isEmpty"/> <suppress files="StringSequence" checks="SpringMethodVisibility" message="isEmpty"/>
<suppress files="ValidatorPropertiesWithDefaultValues\.java" checks="SpringMethodVisibility" /> <suppress files="ValidatorPropertiesWithDefaultValues\.java" checks="SpringMethodVisibility" />
<suppress files="DeprecatedElasticsearchRestClientProperties\.java" checks="SpringMethodVisibility" />
<suppress files="DeprecatedElasticsearchRestClientProperties\.java" checks="SpringMethodVisibility" />
<suppress files="DeprecatedReactiveElasticsearchRestClientProperties\.java" checks="SpringMethodVisibility" />
</suppressions> </suppressions>

Loading…
Cancel
Save