Merge pull request #36213 from poutsma

* pr/36213:
  Add RestClient SSL support
  Overhaul reference documentation for RestClient
  Add RestClient HttpMessageConverters support
  Polish 'Add initial support for RestClient'
  Add initial support for RestClient

Closes gh-36213
pull/35191/head
Phillip Webb 1 year ago
commit f978051127

@ -0,0 +1,55 @@
/*
* Copyright 2012-2023 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.web.client;
import java.util.function.Consumer;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
/**
* An auto-configured {@link RestClientSsl} implementation.
*
* @author Phillip Webb
*/
class AutoConfiguredRestClientSsl implements RestClientSsl {
private final SslBundles sslBundles;
AutoConfiguredRestClientSsl(SslBundles sslBundles) {
this.sslBundles = sslBundles;
}
@Override
public Consumer<RestClient.Builder> fromBundle(String bundleName) {
return fromBundle(this.sslBundles.getBundle(bundleName));
}
@Override
public Consumer<RestClient.Builder> fromBundle(SslBundle bundle) {
return (builder) -> {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS.withSslBundle(bundle);
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings);
builder.requestFactory(requestFactory);
};
}
}

@ -0,0 +1,60 @@
/*
* Copyright 2012-2023 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.web.client;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.client.RestClient;
/**
* {@link RestClientCustomizer} to apply {@link HttpMessageConverter
* HttpMessageConverters}.
*
* @author Phillip Webb
* @since 3.2.0
*/
public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer {
private final Iterable<? extends HttpMessageConverter<?>> messageConverters;
public HttpMessageConvertersRestClientCustomizer(HttpMessageConverter<?>... messageConverters) {
Assert.notNull(messageConverters, "MessageConverters must not be null");
this.messageConverters = Arrays.asList(messageConverters);
}
HttpMessageConvertersRestClientCustomizer(HttpMessageConverters messageConverters) {
this.messageConverters = messageConverters;
}
@Override
public void customize(RestClient.Builder restClientBuilder) {
restClientBuilder.messageConverters(this::configureMessageConverters);
}
private void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
if (this.messageConverters != null) {
messageConverters.clear();
this.messageConverters.forEach(messageConverters::add);
}
}
}

@ -0,0 +1,40 @@
/*
* Copyright 2012-2023 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.web.client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
/**
* {@link SpringBootCondition} that applies only when running in a non-reactive web
* application.
*
* @author Phillip Webb
*/
class NotReactiveWebApplicationCondition extends NoneNestedConditions {
NotReactiveWebApplicationCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
private static class ReactiveWebApplication {
}
}

@ -0,0 +1,69 @@
/*
* Copyright 2012-2023 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.web.client;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Scope;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.client.RestClient;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link RestClient}.
* <p>
* This will produce a {@link org.springframework.web.client.RestClient.Builder
* RestClient.Builder} bean with the {@code prototype} scope, meaning each injection point
* will receive a newly cloned instance of the builder.
*
* @author Arjen Poutsma
* @since 3.2.0
*/
@AutoConfiguration(after = HttpMessageConvertersAutoConfiguration.class)
@ConditionalOnClass(RestClient.class)
@Conditional(NotReactiveWebApplicationCondition.class)
public class RestClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@Order(Ordered.LOWEST_PRECEDENCE)
public HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer(
ObjectProvider<HttpMessageConverters> messageConverters) {
return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique());
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public RestClient.Builder webClientBuilder(ObjectProvider<RestClientCustomizer> customizerProvider) {
RestClient.Builder builder = RestClient.builder()
.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS));
customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
}

@ -0,0 +1,68 @@
/*
* Copyright 2012-2023 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.web.client;
import java.util.function.Consumer;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
/**
* Interface that can be used to {@link RestClient.Builder#apply apply} SSL configuration
* to a {@link org.springframework.web.client.RestClient.Builder RestClient.Builder}.
* <p>
* Typically used as follows: <pre class="code">
* &#064;Bean
* public MyBean myBean(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
* RestClient restClientrestClient= restClientBuilder.apply(ssl.forBundle("mybundle")).build();
* return new MyBean(webClient);
* }
* </pre> NOTE: Apply SSL configuration will replace any previously
* {@link RestClient.Builder#requestFactory configured} {@link ClientHttpRequestFactory}.
* If you need to configure {@link ClientHttpRequestFactory} with more than just SSL
* consider using a {@link ClientHttpRequestFactorySettings} with
* {@link ClientHttpRequestFactories}.
*
* @author Phillip Webb
* @since 3.2.0
*/
public interface RestClientSsl {
/**
* Return a {@link Consumer} that will apply SSL configuration for the named
* {@link SslBundle} to a {@link org.springframework.web.client.RestClient.Builder
* RestClient.Builder}.
* @param bundleName the name of the SSL bundle to apply
* @return a {@link Consumer} to apply the configuration
* @throws NoSuchSslBundleException if a bundle with the provided name does not exist
*/
Consumer<RestClient.Builder> fromBundle(String bundleName) throws NoSuchSslBundleException;
/**
* Return a {@link Consumer} that will apply SSL configuration for the
* {@link SslBundle} to a {@link org.springframework.web.client.RestClient.Builder
* RestClient.Builder}.
* @param bundle the SSL bundle to apply
* @return a {@link Consumer} to apply the configuration
*/
Consumer<RestClient.Builder> fromBundle(SslBundle bundle);
}

@ -21,12 +21,8 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.NotReactiveWebApplicationCondition;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.boot.web.client.RestTemplateRequestCustomizer;
@ -69,17 +65,4 @@ public class RestTemplateAutoConfiguration {
return restTemplateBuilderConfigurer.configure(builder);
}
static class NotReactiveWebApplicationCondition extends NoneNestedConditions {
NotReactiveWebApplicationCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnWebApplication(type = Type.REACTIVE)
private static class ReactiveWebApplication {
}
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2023 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.web.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpMessageConvertersRestClientCustomizer}
*
* @author Phillip Webb
*/
class HttpMessageConvertersRestClientCustomizerTests {
@Test
void createWhenNullMessageConvertersArrayThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new HttpMessageConvertersRestClientCustomizer((HttpMessageConverter<?>[]) null))
.withMessage("MessageConverters must not be null");
}
@Test
void createWhenNullMessageConvertersDoesNotCustomize() {
HttpMessageConverter<?> c0 = mock();
assertThat(apply(new HttpMessageConvertersRestClientCustomizer((HttpMessageConverters) null), c0))
.containsExactly(c0);
}
@Test
void customizeConfiguresMessageConverters() {
HttpMessageConverter<?> c0 = mock();
HttpMessageConverter<?> c1 = mock();
HttpMessageConverter<?> c2 = mock();
assertThat(apply(new HttpMessageConvertersRestClientCustomizer(c1, c2), c0)).containsExactly(c1, c2);
}
@SuppressWarnings("unchecked")
private List<HttpMessageConverter<?>> apply(HttpMessageConvertersRestClientCustomizer customizer,
HttpMessageConverter<?>... converters) {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(Arrays.asList(converters));
RestClient.Builder restClientBuilder = mock();
ArgumentCaptor<Consumer<List<HttpMessageConverter<?>>>> captor = ArgumentCaptor.forClass(Consumer.class);
given(restClientBuilder.messageConverters(captor.capture())).willReturn(restClientBuilder);
customizer.customize(restClientBuilder);
captor.getValue().accept(messageConverters);
return messageConverters;
}
}

@ -0,0 +1,178 @@
/*
* Copyright 2012-2023 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.web.client;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RestClientAutoConfiguration}
*
* @author Arjen Poutsma
*/
class RestClientAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class));
@Test
void shouldCreateBuilder() {
this.contextRunner.run((context) -> {
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
RestClient restClient = builder.build();
assertThat(restClient).isNotNull();
});
}
@Test
void restClientShouldApplyCustomizers() {
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
RestClientCustomizer customizer = context.getBean("webClientCustomizer", RestClientCustomizer.class);
builder.build();
then(customizer).should().customize(any(RestClient.Builder.class));
});
}
@Test
void shouldGetPrototypeScopedBean() {
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
RestClient.Builder firstBuilder = context.getBean(RestClient.Builder.class);
RestClient.Builder secondBuilder = context.getBean(RestClient.Builder.class);
assertThat(firstBuilder).isNotEqualTo(secondBuilder);
});
}
@Test
void shouldNotCreateClientBuilderIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(CustomRestClientBuilderConfig.class).run((context) -> {
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
assertThat(builder).isInstanceOf(MyWebClientBuilder.class);
});
}
@Test
@SuppressWarnings("unchecked")
void restClientWhenMessageConvertersDefinedShouldHaveMessageConverters() {
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.withUserConfiguration(RestClientConfig.class)
.run((context) -> {
RestClient restClient = context.getBean(RestClient.class);
List<HttpMessageConverter<?>> expectedConverters = context.getBean(HttpMessageConverters.class)
.getConverters();
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
.getField(restClient, "messageConverters");
assertThat(actualConverters).containsExactlyElementsOf(expectedConverters);
});
}
@Test
@SuppressWarnings("unchecked")
void restClientWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
this.contextRunner.withUserConfiguration(RestClientConfig.class).run((context) -> {
RestClient restClient = context.getBean(RestClient.class);
RestClient defaultRestClient = RestClient.builder().build();
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
.getField(restClient, "messageConverters");
List<HttpMessageConverter<?>> expectedConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
.getField(defaultRestClient, "messageConverters");
assertThat(actualConverters).hasSameSizeAs(expectedConverters);
});
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
void restClientWhenHasCustomMessageConvertersShouldHaveMessageConverters() {
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
.withUserConfiguration(CustomHttpMessageConverter.class, RestClientConfig.class)
.run((context) -> {
RestClient restClient = context.getBean(RestClient.class);
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
.getField(restClient, "messageConverters");
assertThat(actualConverters).extracting(HttpMessageConverter::getClass)
.contains((Class) CustomHttpMessageConverter.class);
});
}
@Configuration(proxyBeanMethods = false)
static class CodecConfiguration {
@Bean
CodecCustomizer myCodecCustomizer() {
return mock(CodecCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class RestClientCustomizerConfig {
@Bean
RestClientCustomizer webClientCustomizer() {
return mock(RestClientCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomRestClientBuilderConfig {
@Bean
MyWebClientBuilder myWebClientBuilder() {
return mock(MyWebClientBuilder.class);
}
}
interface MyWebClientBuilder extends RestClient.Builder {
}
@Configuration(proxyBeanMethods = false)
static class RestClientConfig {
@Bean
RestClient restClient(RestClient.Builder restClientBuilder) {
return restClientBuilder.build();
}
}
static class CustomHttpMessageConverter extends StringHttpMessageConverter {
}
}

@ -1020,3 +1020,6 @@ howto.testing.testcontainers.dynamic-properties=features.testing.testcontainers.
# gh-32905
container-images.efficient-images.unpacking=deployment.efficient.unpacking
# Spring Boot 3.1 - 3.2 migrations
io.rest-client.resttemplate.http-client=io.rest-client.clienthttprequestfactory

@ -1,80 +1,23 @@
[[io.rest-client]]
== Calling REST Services
If your application calls remote REST services, Spring Boot makes that very convenient using a `RestTemplate` or a `WebClient`.
[[io.rest-client.resttemplate]]
=== RestTemplate
If you need to call remote REST services from your application, you can use the Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class.
Since `RestTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `RestTemplate` bean.
It does, however, auto-configure a `RestTemplateBuilder`, which can be used to create `RestTemplate` instances when needed.
The auto-configured `RestTemplateBuilder` ensures that sensible `HttpMessageConverters` are applied to `RestTemplate` instances.
The following code shows a typical example:
include::code:MyService[]
`RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`.
For example, to add BASIC authentication support, you can use `builder.basicAuthentication("user", "password").build()`.
[[io.rest-client.resttemplate.http-client]]
==== RestTemplate HTTP Client
Spring Boot will auto-detect which HTTP client to use with `RestTemplate` depending on the libraries available on the application classpath.
In order of preference, the following clients are supported:
. Apache HttpClient
. OkHttp
. Jetty HttpClient
. Simple JDK client (`HttpURLConnection`)
If multiple clients are available on the classpath, the most preferred client will be used.
[[io.rest-client.resttemplate.customization]]
==== RestTemplate Customization
There are three main approaches to `RestTemplate` customization, depending on how broadly you want the customizations to apply.
To make the scope of any customizations as narrow as possible, inject the auto-configured `RestTemplateBuilder` and then call its methods as required.
Each method call returns a new `RestTemplateBuilder` instance, so the customizations only affect this use of the builder.
To make an application-wide, additive customization, use a `RestTemplateCustomizer` bean.
All such beans are automatically registered with the auto-configured `RestTemplateBuilder` and are applied to any templates that are built with it.
The following example shows a customizer that configures the use of a proxy for all hosts except `192.168.0.5`:
include::code:MyRestTemplateCustomizer[]
Finally, you can define your own `RestTemplateBuilder` bean.
Doing so will replace the auto-configured builder.
If you want any `RestTemplateCustomizer` beans to be applied to your custom builder, as the auto-configuration would have done, configure it using a `RestTemplateBuilderConfigurer`.
The following example exposes a `RestTemplateBuilder` that matches what Spring Boot's auto-configuration would have done, except that custom connect and read timeouts are also specified:
include::code:MyRestTemplateBuilderConfiguration[]
The most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean without using a configurer.
In addition to replacing the auto-configured builder, this also prevents any `RestTemplateCustomizer` beans from being used.
[[io.rest-client.resttemplate.ssl]]
==== RestTemplate SSL Support
If you need custom SSL configuration on the `RestTemplate`, you can apply an <<features#features.ssl.bundles,SSL bundle>> to the `RestTemplateBuilder` as shown in this example:
include::code:MyService[]
Spring Boot provides various convenient ways to call remote REST services.
If you are developing a non-blocking reactive application and you're using Spring WebFlux, then you can use `WebClient`.
If you prefer blocking APIs then you can use `RestClient` or `RestTemplate`.
[[io.rest-client.webclient]]
=== WebClient
If you have Spring WebFlux on your classpath, you can also choose to use `WebClient` to call remote REST services.
Compared to `RestTemplate`, this client has a more functional feel and is fully reactive.
If you have Spring WebFlux on your classpath we recommend that you use `WebClient` to call remote REST services.
The `WebClient` interface provides a functional style API and is fully reactive.
You can learn more about the `WebClient` in the dedicated {spring-framework-docs}/web-reactive.html#webflux-client[section in the Spring Framework docs].
Spring Boot creates and pre-configures a `WebClient.Builder` for you.
TIP: If you are not writing a reactive Spring WebFlux application you can use the a <<io#io.rest-client.restclient,`RestClient`>> instead of a `WebClient`.
This provides a similar functional API, but is blocking rather than reactive.
Spring Boot creates and pre-configures a prototype `WebClient.Builder` bean for you.
It is strongly advised to inject it in your components and use it to create `WebClient` instances.
Spring Boot is configuring that builder to share HTTP resources, reflect codecs setup in the same fashion as the server ones (see <<web#web.reactive.webflux.httpcodecs,WebFlux HTTP codecs auto-configuration>>), and more.
Spring Boot is configuring that builder to share HTTP resources and reflect codecs setup in the same fashion as the server ones (see <<web#web.reactive.webflux.httpcodecs,WebFlux HTTP codecs auto-configuration>>), and more.
The following code shows a typical example:
@ -131,3 +74,115 @@ The following code shows a typical example:
include::code:MyService[]
[[io.rest-client.restclient]]
=== RestClient
If you are not using Spring WebFlux or Project Reactor in your application we recommend that you use `RestClient` to call remote REST services.
The `RestClient` interface provides a functional style blocking API.
Spring Boot creates and pre-configures a prototype `RestClient.Builder` bean for you.
It is strongly advised to inject it in your components and use it to create `RestClient` instances.
Spring Boot is configuring that builder with `HttpMessageConverters` and an appropriate `ClientHttpRequestFactory`.
The following code shows a typical example:
include::code:MyService[]
[[io.rest-client.restclient.customization]]
==== RestClient Customization
There are three main approaches to `RestClient` customization, depending on how broadly you want the customizations to apply.
To make the scope of any customizations as narrow as possible, inject the auto-configured `RestClient.Builder` and then call its methods as required.
`RestClient.Builder` instances are stateful: Any change on the builder is reflected in all clients subsequently created with it.
If you want to create several clients with the same builder, you can also consider cloning the builder with `RestClient.Builder other = builder.clone();`.
To make an application-wide, additive customization to all `RestClient.Builder` instances, you can declare `RestClientCustomizer` beans and change the `RestClient.Builder` locally at the point of injection.
Finally, you can fall back to the original API and use `RestClient.create()`.
In that case, no auto-configuration or `RestClientCustomizer` is applied.
[[io.rest-client.restclient.ssl]]
==== RestClient SSL Support
If you need custom SSL configuration on the `ClientHttpRequestFactory` used by the `RestClient`, you can inject a `RestClientSsl` instance that can be used with the builder's `apply` method.
The `RestClientSsl` interface provides access to any <<features#features.ssl.bundles,SSL bundles>> that you have defined in your `application.properties` or `application.yaml` file.
The following code shows a typical example:
include::code:MyService[]
If you need to apply other customization in addition to an SSL bundle, you can use the `ClientHttpRequestFactorySettings` class with `ClientHttpRequestFactories`:
include::code:settings/MyService[]
[[io.rest-client.resttemplate]]
=== RestTemplate
Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class predates `RestClient` and is the classic way that many applications use to call remote REST services.
You might choose to use `RestTemplate` when you have existing code that you don't want to migrate to `RestClient`, or because you're already familiar with the `RestTemplate` API.
Since `RestTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `RestTemplate` bean.
It does, however, auto-configure a `RestTemplateBuilder`, which can be used to create `RestTemplate` instances when needed.
The auto-configured `RestTemplateBuilder` ensures that sensible `HttpMessageConverters` and an appropriate `ClientHttpRequestFactory` are applied to `RestTemplate` instances.
The following code shows a typical example:
include::code:MyService[]
`RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`.
For example, to add BASIC authentication support, you can use `builder.basicAuthentication("user", "password").build()`.
[[io.rest-client.resttemplate.customization]]
==== RestTemplate Customization
There are three main approaches to `RestTemplate` customization, depending on how broadly you want the customizations to apply.
To make the scope of any customizations as narrow as possible, inject the auto-configured `RestTemplateBuilder` and then call its methods as required.
Each method call returns a new `RestTemplateBuilder` instance, so the customizations only affect this use of the builder.
To make an application-wide, additive customization, use a `RestTemplateCustomizer` bean.
All such beans are automatically registered with the auto-configured `RestTemplateBuilder` and are applied to any templates that are built with it.
The following example shows a customizer that configures the use of a proxy for all hosts except `192.168.0.5`:
include::code:MyRestTemplateCustomizer[]
Finally, you can define your own `RestTemplateBuilder` bean.
Doing so will replace the auto-configured builder.
If you want any `RestTemplateCustomizer` beans to be applied to your custom builder, as the auto-configuration would have done, configure it using a `RestTemplateBuilderConfigurer`.
The following example exposes a `RestTemplateBuilder` that matches what Spring Boot's auto-configuration would have done, except that custom connect and read timeouts are also specified:
include::code:MyRestTemplateBuilderConfiguration[]
The most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean without using a configurer.
In addition to replacing the auto-configured builder, this also prevents any `RestTemplateCustomizer` beans from being used.
[[io.rest-client.resttemplate.ssl]]
==== RestTemplate SSL Support
If you need custom SSL configuration on the `RestTemplate`, you can apply an <<features#features.ssl.bundles,SSL bundle>> to the `RestTemplateBuilder` as shown in this example:
include::code:MyService[]
[[io.rest-client.clienthttprequestfactory]]
=== HTTP Client Detection for RestClient and RestTemplate
Spring Boot will auto-detect which HTTP client to use with `RestClient` and `RestTemplate` depending on the libraries available on the application classpath.
In order of preference, the following clients are supported:
. Apache HttpClient
. OkHttp
. Jetty HttpClient
. Simple JDK client (`HttpURLConnection`)
If multiple clients are available on the classpath, the most preferred client will be used.

@ -0,0 +1,21 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient;
public class Details {
}

@ -0,0 +1,35 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class MyService {
private final RestClient restClient;
public MyService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.baseUrl("https://example.org").build();
}
public Details someRestCall(String name) {
return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class);
}
}

@ -0,0 +1,36 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl;
import org.springframework.boot.autoconfigure.web.client.RestClientSsl;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class MyService {
private final RestClient restClient;
public MyService(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
this.restClient = restClientBuilder.baseUrl("https://example.org").apply(ssl.fromBundle("mybundle")).build();
}
public Details someRestCall(String name) {
return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class);
}
}

@ -0,0 +1,21 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings;
public class Details {
}

@ -0,0 +1,45 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings;
import java.time.Duration;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class MyService {
private final RestClient restClient;
public MyService(RestClient.Builder restClientBuilder, SslBundles sslBundles) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
.withReadTimeout(Duration.ofMinutes(2))
.withSslBundle(sslBundles.getBundle("mybundle"));
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings);
this.restClient = restClientBuilder.baseUrl("https://example.org").requestFactory(requestFactory).build();
}
public Details someRestCall(String name) {
return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class);
}
}

@ -0,0 +1,19 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient
class Details

@ -0,0 +1,38 @@
/*
* Copyright 2012-2022 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.docs.io.restclient.restclient
import org.springframework.boot.docs.io.restclient.restclient.ssl.Details
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
@Service
class MyService(restClientBuilder: RestClient.Builder) {
private val restClient: RestClient
init {
restClient = restClientBuilder.baseUrl("https://example.org").build()
}
fun someRestCall(name: String?): Details {
return restClient.get().uri("/{name}/details", name)
.retrieve().body(Details::class.java)!!
}
}

@ -0,0 +1,19 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl
class Details

@ -0,0 +1,39 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl
import org.springframework.boot.autoconfigure.web.client.RestClientSsl
import org.springframework.boot.docs.io.restclient.restclient.ssl.settings.Details
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
@Service
class MyService(restClientBuilder: RestClient.Builder, ssl: RestClientSsl) {
private val restClient: RestClient
init {
restClient = restClientBuilder.baseUrl("https://example.org")
.apply(ssl.fromBundle("mybundle")).build()
}
fun someRestCall(name: String?): Details {
return restClient.get().uri("/{name}/details", name)
.retrieve().body(Details::class.java)!!
}
}

@ -0,0 +1,19 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings
class Details

@ -0,0 +1,46 @@
/*
* Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings
import org.springframework.boot.ssl.SslBundles
import org.springframework.boot.web.client.ClientHttpRequestFactories
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
import java.time.Duration
@Service
class MyService(restClientBuilder: RestClient.Builder, sslBundles: SslBundles) {
private val restClient: RestClient
init {
val settings = ClientHttpRequestFactorySettings.DEFAULTS
.withReadTimeout(Duration.ofMinutes(2))
.withSslBundle(sslBundles.getBundle("mybundle"))
val requestFactory = ClientHttpRequestFactories.get(settings)
restClient = restClientBuilder
.baseUrl("https://example.org")
.requestFactory(requestFactory).build()
}
fun someRestCall(name: String?): Details {
return restClient.get().uri("/{name}/details", name).retrieve().body(Details::class.java)!!
}
}

@ -0,0 +1,38 @@
/*
* Copyright 2012-2023 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.web.client;
import org.springframework.web.client.RestClient;
/**
* Callback interface that can be used to customize a
* {@link org.springframework.web.client.RestClient.Builder RestClient.Builder}.
*
* @author Arjen Poutsma
* @since 3.2.0
*/
@FunctionalInterface
public interface RestClientCustomizer {
/**
* Callback to customize a {@link org.springframework.web.client.RestClient.Builder
* RestClient.Builder} instance.
* @param restClientBuilder the client builder to customize
*/
void customize(RestClient.Builder restClientBuilder);
}
Loading…
Cancel
Save