Commit f701d97b authored by Vedran Pavic's avatar Vedran Pavic Committed by Brian Clozel

Improve Elasticsearch RestClient customization capabilities

At present, RestClientBuilderCustomizer allows general customization of RestClientBuilder.
This is troublesome for users that want to customize `HttpAsyncClientBuilder` and
`RequestConfig.Builder` since those are set on the `RestClientBuilder`. By customizing
those two builders user lose out on Spring Boot's support for binding username, password,
connection-timeout and read-timeout properties from `"spring.elasticsearch.rest"` namespace.

This commit enhances the `RestClientBuilderCustomizer` with support for customizing
`HttpAsyncClientBuilder` and `RequestConfig.Builder` by providing additional `customize`
methods that accept the aforementioned builders. Both new methods are optional as they have
no-op default implementations.

See gh-20994
parent 8de00277
...@@ -23,7 +23,9 @@ import org.apache.http.auth.AuthScope; ...@@ -23,7 +23,9 @@ import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials; import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider; import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.RestHighLevelClient;
...@@ -40,32 +42,30 @@ import org.springframework.context.annotation.Configuration; ...@@ -40,32 +42,30 @@ import org.springframework.context.annotation.Configuration;
* *
* @author Brian Clozel * @author Brian Clozel
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Vedran Pavic
*/ */
class ElasticsearchRestClientConfigurations { class ElasticsearchRestClientConfigurations {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(RestClientBuilder.class)
static class RestClientBuilderConfiguration { static class RestClientBuilderConfiguration {
@Bean @Bean
@ConditionalOnMissingBean RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) {
return new DefaultRestClientBuilderCustomizer(properties);
}
@Bean
RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties,
ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) { ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) {
HttpHost[] hosts = properties.getUris().stream().map(HttpHost::create).toArray(HttpHost[]::new); HttpHost[] hosts = properties.getUris().stream().map(HttpHost::create).toArray(HttpHost[]::new);
RestClientBuilder builder = RestClient.builder(hosts); RestClientBuilder builder = RestClient.builder(hosts);
PropertyMapper map = PropertyMapper.get(); builder.setHttpClientConfigCallback((httpClientBuilder) -> {
map.from(properties::getUsername).whenHasText().to((username) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder));
CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); return httpClientBuilder;
Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(),
properties.getPassword());
credentialsProvider.setCredentials(AuthScope.ANY, credentials);
builder.setHttpClientConfigCallback(
(httpClientBuilder) -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
}); });
builder.setRequestConfigCallback((requestConfigBuilder) -> { builder.setRequestConfigCallback((requestConfigBuilder) -> {
map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(requestConfigBuilder));
.to(requestConfigBuilder::setConnectTimeout);
map.from(properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis)
.to(requestConfigBuilder::setSocketTimeout);
return requestConfigBuilder; return requestConfigBuilder;
}); });
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
...@@ -108,4 +108,39 @@ class ElasticsearchRestClientConfigurations { ...@@ -108,4 +108,39 @@ class ElasticsearchRestClientConfigurations {
} }
static class DefaultRestClientBuilderCustomizer implements RestClientBuilderCustomizer {
private static final PropertyMapper map = PropertyMapper.get();
private final ElasticsearchRestClientProperties properties;
DefaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) {
this.properties = properties;
}
@Override
public void customize(RestClientBuilder builder) {
}
@Override
public void customize(HttpAsyncClientBuilder builder) {
map.from(this.properties::getUsername).whenHasText().to((username) -> {
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
Credentials credentials = new UsernamePasswordCredentials(this.properties.getUsername(),
this.properties.getPassword());
credentialsProvider.setCredentials(AuthScope.ANY, credentials);
builder.setDefaultCredentialsProvider(credentialsProvider);
});
}
@Override
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)
.to(builder::setSocketTimeout);
}
}
} }
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure.elasticsearch; package org.springframework.boot.autoconfigure.elasticsearch;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestClientBuilder;
/** /**
...@@ -24,6 +26,7 @@ import org.elasticsearch.client.RestClientBuilder; ...@@ -24,6 +26,7 @@ import org.elasticsearch.client.RestClientBuilder;
* retaining default auto-configuration. * retaining default auto-configuration.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Vedran Pavic
* @since 2.1.0 * @since 2.1.0
*/ */
@FunctionalInterface @FunctionalInterface
...@@ -35,4 +38,18 @@ public interface RestClientBuilderCustomizer { ...@@ -35,4 +38,18 @@ public interface RestClientBuilderCustomizer {
*/ */
void customize(RestClientBuilder builder); void customize(RestClientBuilder builder);
/**
* Customize the {@link HttpAsyncClientBuilder}.
* @param builder the builder
*/
default void customize(HttpAsyncClientBuilder builder) {
}
/**
* Customize the {@link RequestConfig.Builder}.
* @param builder the builder
*/
default void customize(RequestConfig.Builder builder) {
}
} }
...@@ -20,6 +20,8 @@ import java.time.Duration; ...@@ -20,6 +20,8 @@ import java.time.Duration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RequestOptions;
...@@ -36,7 +38,6 @@ import org.springframework.boot.test.context.FilteredClassLoader; ...@@ -36,7 +38,6 @@ import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
...@@ -45,6 +46,7 @@ import static org.mockito.Mockito.mock; ...@@ -45,6 +46,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link ElasticsearchRestClientAutoConfiguration}. * Tests for {@link ElasticsearchRestClientAutoConfiguration}.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Vedran Pavic
*/ */
@Testcontainers(disabledWithoutDocker = true) @Testcontainers(disabledWithoutDocker = true)
class ElasticsearchRestClientAutoConfigurationTests { class ElasticsearchRestClientAutoConfigurationTests {
...@@ -53,7 +55,7 @@ class ElasticsearchRestClientAutoConfigurationTests { ...@@ -53,7 +55,7 @@ class ElasticsearchRestClientAutoConfigurationTests {
static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10)); .withStartupTimeout(Duration.ofMinutes(10));
private ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class)); .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class));
@Test @Test
...@@ -106,6 +108,8 @@ class ElasticsearchRestClientAutoConfigurationTests { ...@@ -106,6 +108,8 @@ class ElasticsearchRestClientAutoConfigurationTests {
assertThat(context).hasSingleBean(RestClient.class); assertThat(context).hasSingleBean(RestClient.class);
RestClient restClient = context.getBean(RestClient.class); RestClient restClient = context.getBean(RestClient.class);
assertThat(restClient).hasFieldOrPropertyWithValue("pathPrefix", "/test"); assertThat(restClient).hasFieldOrPropertyWithValue("pathPrefix", "/test");
assertThat(restClient).extracting("client.connmgr.pool.maxTotal").isEqualTo(100);
assertThat(restClient).extracting("client.defaultConfig.cookieSpec").isEqualTo("rfc6265-lax");
}); });
} }
...@@ -130,10 +134,10 @@ class ElasticsearchRestClientAutoConfigurationTests { ...@@ -130,10 +134,10 @@ class ElasticsearchRestClientAutoConfigurationTests {
} }
private static void assertTimeouts(RestClient restClient, Duration connectTimeout, Duration readTimeout) { private static void assertTimeouts(RestClient restClient, Duration connectTimeout, Duration readTimeout) {
Object client = ReflectionTestUtils.getField(restClient, "client"); assertThat(restClient).extracting("client.defaultConfig.socketTimeout")
Object config = ReflectionTestUtils.getField(client, "defaultConfig"); .isEqualTo(Math.toIntExact(readTimeout.toMillis()));
assertThat(config).hasFieldOrPropertyWithValue("socketTimeout", Math.toIntExact(readTimeout.toMillis())); assertThat(restClient).extracting("client.defaultConfig.connectTimeout")
assertThat(config).hasFieldOrPropertyWithValue("connectTimeout", Math.toIntExact(connectTimeout.toMillis())); .isEqualTo(Math.toIntExact(connectTimeout.toMillis()));
} }
@Test @Test
...@@ -167,7 +171,24 @@ class ElasticsearchRestClientAutoConfigurationTests { ...@@ -167,7 +171,24 @@ class ElasticsearchRestClientAutoConfigurationTests {
@Bean @Bean
RestClientBuilderCustomizer myCustomizer() { RestClientBuilderCustomizer myCustomizer() {
return (builder) -> builder.setPathPrefix("/test"); return new RestClientBuilderCustomizer() {
@Override
public void customize(RestClientBuilder builder) {
builder.setPathPrefix("/test");
}
@Override
public void customize(HttpAsyncClientBuilder builder) {
builder.setMaxConnTotal(100);
}
@Override
public void customize(RequestConfig.Builder builder) {
builder.setCookieSpec("rfc6265-lax");
}
};
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment