diff --git a/spring-boot-project/spring-boot-autoconfigure/pom.xml b/spring-boot-project/spring-boot-autoconfigure/pom.xml index 7d46adb2320..d86bef25c03 100755 --- a/spring-boot-project/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-project/spring-boot-autoconfigure/pom.xml @@ -305,6 +305,16 @@ ehcache true + + org.elasticsearch.client + elasticsearch-rest-client + true + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + true + org.freemarker freemarker diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfiguration.java new file mode 100644 index 00000000000..39861f044a0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfiguration.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.rest; + +import java.util.Collections; +import java.util.List; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.elasticsearch.client.RestHighLevelClient; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-Configuration} + * for Elasticseach REST clients. + * + * @author Brian Clozel + * @since 2.1.0 + */ +@Configuration +@ConditionalOnClass(RestClient.class) +@EnableConfigurationProperties(RestClientProperties.class) +public class RestClientAutoConfiguration { + + + private final RestClientProperties properties; + + private final List builderCustomizers; + + public RestClientAutoConfiguration(RestClientProperties properties, + ObjectProvider> builderCustomizers) { + this.properties = properties; + this.builderCustomizers = builderCustomizers.getIfAvailable(Collections::emptyList); + } + + @Bean(destroyMethod = "close") + @ConditionalOnMissingBean + public RestClient restClient() { + RestClientBuilder builder = configureBuilder(); + return builder.build(); + } + + protected RestClientBuilder configureBuilder() { + HttpHost[] hosts = this.properties.getUris().stream() + .map(HttpHost::create).toArray(HttpHost[]::new); + RestClientBuilder builder = RestClient.builder(hosts); + PropertyMapper map = PropertyMapper.get(); + 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.setHttpClientConfigCallback(httpClientBuilder -> + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + }); + this.builderCustomizers.forEach((customizer) -> customizer.customize(builder)); + return builder; + } + + @Configuration + @ConditionalOnClass(RestHighLevelClient.class) + public static class RestHighLevelClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public RestHighLevelClient restHighLevelClient(RestClient restClient) { + return new RestHighLevelClient(restClient); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientBuilderCustomizer.java new file mode 100644 index 00000000000..8ed0f1f7771 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.rest; + +import org.elasticsearch.client.RestClientBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link org.elasticsearch.client.RestClient} via a {@link RestClientBuilder} whilst + * retaining default auto-configuration. + * + * @author Brian Clozel + * @since 2.1.0 + */ +@FunctionalInterface +public interface RestClientBuilderCustomizer { + + /** + * Customize the {@link RestClientBuilder}. + * @param builder the builder to customize + */ + void customize(RestClientBuilder builder); +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientProperties.java new file mode 100644 index 00000000000..33d876b3848 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientProperties.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.rest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Elasticsearch REST clients. + * + * @author Brian Clozel + * @since 2.1.0 + */ +@ConfigurationProperties(prefix = "spring.elasticsearch.rest") +public class RestClientProperties { + + /** + * 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; + + + + 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; + } +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/package-info.java new file mode 100644 index 00000000000..b6252cc5807 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2018 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 + * + * http://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. + */ + +/** + * Auto-configuration for Elasticsearch REST clients. + */ +package org.springframework.boot.autoconfigure.elasticsearch.rest; 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 5da488a05fc..fca0a274d9e 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 @@ -212,6 +212,12 @@ "http://localhost:9200" ] }, + { + "name": "spring.elasticsearch.rest.uris", + "defaultValue": [ + "http://localhost:9200" + ] + }, { "name": "spring.info.build.location", "defaultValue": "classpath:META-INF/build-info.properties" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index b140c1f7501..a88022db1f2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -56,6 +56,7 @@ org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfigura org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\ org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\ +org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java index 533599e9d38..58aff9a8227 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java @@ -25,10 +25,8 @@ import io.searchbox.action.Action; import io.searchbox.client.JestClient; import io.searchbox.client.JestResult; import io.searchbox.client.http.JestHttpClient; +import io.searchbox.core.Get; import io.searchbox.core.Index; -import io.searchbox.core.Search; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.builder.SearchSourceBuilder; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -74,9 +72,8 @@ public class JestAutoConfigurationTests { @Test public void jestClientOnLocalhostByDefault() { - this.contextRunner - .run((context) -> assertThat(context.getBeansOfType(JestClient.class)) - .hasSize(1)); + this.contextRunner.run((context) -> + assertThat(context).hasSingleBean(JestClient.class)); } @Test @@ -84,8 +81,7 @@ public class JestAutoConfigurationTests { this.contextRunner.withUserConfiguration(CustomJestClient.class) .withPropertyValues( "spring.elasticsearch.jest.uris[0]=http://localhost:9200") - .run((context) -> assertThat(context.getBeansOfType(JestClient.class)) - .hasSize(1)); + .run((context) -> assertThat(context).hasSingleBean(JestClient.class)); } @Test @@ -134,15 +130,11 @@ public class JestAutoConfigurationTests { Map source = new HashMap<>(); source.put("a", "alpha"); source.put("b", "bravo"); - Index index = new Index.Builder(source).index("foo").type("bar") - .build(); + Index index = new Index.Builder(source).index("foo") + .type("bar").id("1").build(); execute(client, index); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.matchQuery("a", "alpha")); - assertThat(execute(client, - new Search.Builder(searchSourceBuilder.toString()) - .addIndex("foo").build()).getResponseCode()) - .isEqualTo(200); + Get getRequest = new Get.Builder("foo", "1").build(); + assertThat(execute(client, getRequest).getResponseCode()).isEqualTo(200); })); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfigurationTests.java new file mode 100644 index 00000000000..db10850d526 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfigurationTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.rest; + + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchNodeTemplate; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestClientAutoConfiguration} + * + * @author Brian Clozel + */ +public class RestClientAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class)); + + @Test + public void configureShouldCreateBothRestClientVariants() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(RestClient.class) + .hasSingleBean(RestHighLevelClient.class); + }); + } + + @Test + public void configureWhenCustomClientShouldBackOff() { + this.contextRunner + .withUserConfiguration(CustomRestClientConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(RestClient.class) + .hasBean("customRestClient"); + }); + } + + @Test + public void configureWhenBuilderCustomizerShouldApply() { + this.contextRunner + .withUserConfiguration(BuilderCustomizerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(RestClient.class); + RestClient restClient = context.getBean(RestClient.class); + Field field = ReflectionUtils.findField(RestClient.class, + "maxRetryTimeoutMillis"); + ReflectionUtils.makeAccessible(field); + assertThat(ReflectionUtils.getField(field, restClient)) + .isEqualTo(42L); + }); + } + + @Test + public void restClientCanQueryElasticsearchNode() { + new ElasticsearchNodeTemplate().doWithNode((node) -> this.contextRunner + .withPropertyValues("spring.elasticsearch.rest.uris=http://localhost:" + + node.getHttpPort()) + .run((context) -> { + RestHighLevelClient client = context.getBean(RestHighLevelClient.class); + Map source = new HashMap<>(); + source.put("a", "alpha"); + source.put("b", "bravo"); + IndexRequest index = new IndexRequest("foo", "bar", "1") + .source(source); + client.index(index); + GetRequest getRequest = new GetRequest("foo", "bar", "1"); + assertThat(client.get(getRequest).isExists()).isTrue(); + })); + } + + @Configuration + static class CustomRestClientConfiguration { + + @Bean + public RestClient customRestClient() { + return mock(RestClient.class); + } + } + + @Configuration + static class BuilderCustomizerConfiguration { + + @Bean + public RestClientBuilderCustomizer myCustomizer() { + return builder -> builder.setMaxRetryTimeoutMillis(42); + } + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 8daeddc37c6..58a9ee0ca92 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -1757,6 +1757,16 @@ transport-netty4-client ${elasticsearch.version} + + org.elasticsearch.client + elasticsearch-rest-client + ${elasticsearch.version} + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + ${elasticsearch.version} + org.firebirdsql.jdbc jaybird-jdk17 diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index d614d1e6bcd..ce103d95e0f 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -717,6 +717,11 @@ content into your application. Rather, pick only the properties that you need. spring.elasticsearch.jest.uris=http://localhost:9200 # Comma-separated list of the Elasticsearch instances to use. spring.elasticsearch.jest.username= # Login username. + # Elasticsearch REST clients ({sc-spring-boot-autoconfigure}/elasticsearch/rest/RestClientProperties.{sc-ext}[RestClientProperties]) + spring.elasticsearch.rest.password= # Credentials username. + spring.elasticsearch.rest.uris=http://localhost:9200 # Comma-separated list of the Elasticsearch instances to use. + spring.elasticsearch.rest.username= # Credentials password. + # H2 Web Console ({sc-spring-boot-autoconfigure}/h2/H2ConsoleProperties.{sc-ext}[H2ConsoleProperties]) spring.h2.console.enabled=false # Whether to enable the console. spring.h2.console.path=/h2-console # Path at which the console is available. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 9e19cb9c2bb..c464502ca25 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -4221,14 +4221,45 @@ https://projects.spring.io/spring-data-solr/[reference documentation]. [[boot-features-elasticsearch]] === Elasticsearch -http://www.elasticsearch.org/[Elasticsearch] is an open source, distributed, real-time -search and analytics engine. Spring Boot offers basic auto-configuration for -Elasticsearch and the abstractions on top of it provided by -https://github.com/spring-projects/spring-data-elasticsearch[Spring Data Elasticsearch]. -There is a `spring-boot-starter-data-elasticsearch` "`Starter`" for collecting the -dependencies in a convenient way. Spring Boot also supports -https://github.com/searchbox-io/Jest[Jest]. +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 supports several HTTP clients: + +* The official Java "Low Level" and "High Level" REST clients +* https://github.com/searchbox-io/Jest[Jest] + +The transport client is still being used by +https://github.com/spring-projects/spring-data-elasticsearch[Spring Data Elasticsearch], +which you can start using with the `spring-boot-starter-data-elasticsearch` "`Starter`". + + +[[boot-features-connecting-to-elasticsearch-rest]] +==== Connecting to Elasticsearch by 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. + +If you have the `org.elasticsearch.client:elasticsearch-rest-client` dependency on the +classpath, Spring Boot will auto-configure and register a `RestClient` bean that +by default targets `http://localhost:9200`. +You can further tune how `RestClient` is configured, as shown in the following example: + +[source,properties,indent=0] +---- + spring.elasticsearch.rest.uris=http://search.example.com:9200 + spring.elasticsearch.rest.username=user + spring.elasticsearch.rest.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 `RestClient` bean. + +If you have the `org.elasticsearch.client:elasticsearch-rest-high-level-client` dependency +on the classpath, Spring Boot will auto-configure a `RestHighLevelClient`, which wraps +any existing `RestClient` bean, reusing its HTTP configuration. [[boot-features-connecting-to-elasticsearch-jest]]