Browse Source

Introduce RestClient

This commit introduces the RestClient, a synchronous HTTP client that
offers an API similar to WebClient, using the same infrastructure (i.e.
request factory, error handler, interceptors, etc) as RestTemplate.

Closes gh-29552
pull/30820/head
Arjen Poutsma 3 years ago
parent
commit
0820210c7c
  1. 618
      spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java
  2. 375
      spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
  3. 751
      spring-web/src/main/java/org/springframework/web/client/RestClient.java
  4. 52
      spring-web/src/main/java/org/springframework/web/client/RestClientUtils.java
  5. 156
      spring-web/src/main/java/org/springframework/web/client/StatusHandler.java
  6. 2
      spring-web/src/main/java/org/springframework/web/client/package-info.java
  7. 758
      spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java

618
spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java

@ -0,0 +1,618 @@ @@ -0,0 +1,618 @@
/*
* Copyright 2002-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.web.client;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.nio.charset.Charset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInitializer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory;
/**
* Default implementation of {@link RestClient}.
*
* @author Arjen Poutsma
* @since 6.1
*/
final class DefaultRestClient implements RestClient {
private static final Log logger = LogFactory.getLog(DefaultRestClient.class);
private static final String URI_TEMPLATE_ATTRIBUTE = RestClient.class.getName() + ".uriTemplate";
private final ClientHttpRequestFactory clientRequestFactory;
@Nullable
private volatile ClientHttpRequestFactory interceptingRequestFactory;
@Nullable
private final List<ClientHttpRequestInitializer> initializers;
@Nullable
private final List<ClientHttpRequestInterceptor> interceptors;
private final UriBuilderFactory uriBuilderFactory;
@Nullable
private final HttpHeaders defaultHeaders;
private final List<StatusHandler> defaultStatusHandlers;
private final DefaultRestClientBuilder builder;
private final List<HttpMessageConverter<?>> messageConverters;
DefaultRestClient(ClientHttpRequestFactory clientRequestFactory, @Nullable List<ClientHttpRequestInterceptor> interceptors,
@Nullable List<ClientHttpRequestInitializer> initializers,
UriBuilderFactory uriBuilderFactory,
@Nullable HttpHeaders defaultHeaders,
@Nullable List<StatusHandler> statusHandlers,
List<HttpMessageConverter<?>> messageConverters,
DefaultRestClientBuilder builder) {
this.clientRequestFactory = clientRequestFactory;
this.initializers = initializers;
this.interceptors = interceptors;
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = defaultHeaders;
this.defaultStatusHandlers = (statusHandlers != null) ? new ArrayList<>(statusHandlers) : new ArrayList<>();
this.messageConverters = messageConverters;
this.builder = builder;
}
@Override
public RequestHeadersUriSpec<?> get() {
return methodInternal(HttpMethod.GET);
}
@Override
public RequestHeadersUriSpec<?> head() {
return methodInternal(HttpMethod.HEAD);
}
@Override
public RequestBodyUriSpec post() {
return methodInternal(HttpMethod.POST);
}
@Override
public RequestBodyUriSpec put() {
return methodInternal(HttpMethod.PUT);
}
@Override
public RequestBodyUriSpec patch() {
return methodInternal(HttpMethod.PATCH);
}
@Override
public RequestHeadersUriSpec<?> delete() {
return methodInternal(HttpMethod.DELETE);
}
@Override
public RequestHeadersUriSpec<?> options() {
return methodInternal(HttpMethod.OPTIONS);
}
@Override
public RequestBodyUriSpec method(HttpMethod method) {
Assert.notNull(method, "Method must not be null");
return methodInternal(method);
}
private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) {
return new DefaultRequestBodyUriSpec(httpMethod);
}
@Override
public Builder mutate() {
return new DefaultRestClientBuilder(this.builder);
}
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
private final HttpMethod httpMethod;
@Nullable
private URI uri;
@Nullable
private HttpHeaders headers;
@Nullable
private InternalBody body;
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
@Nullable
private Consumer<ClientHttpRequest> httpRequestConsumer;
public DefaultRequestBodyUriSpec(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
}
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public RequestBodySpec uri(String uriTemplate, Map<String, ?> uriVariables) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public RequestBodySpec uri(String uriTemplate, Function<UriBuilder, URI> uriFunction) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(uriFunction.apply(DefaultRestClient.this.uriBuilderFactory.uriString(uriTemplate)));
}
@Override
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
return uri(uriFunction.apply(DefaultRestClient.this.uriBuilderFactory.builder()));
}
@Override
public RequestBodySpec uri(URI uri) {
this.uri = uri;
return this;
}
private HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = new HttpHeaders();
}
return this.headers;
}
@Override
public DefaultRequestBodyUriSpec header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
getHeaders().add(headerName, headerValue);
}
return this;
}
@Override
public DefaultRequestBodyUriSpec headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(getHeaders());
return this;
}
@Override
public DefaultRequestBodyUriSpec accept(MediaType... acceptableMediaTypes) {
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
return this;
}
@Override
public DefaultRequestBodyUriSpec acceptCharset(Charset... acceptableCharsets) {
getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets));
return this;
}
@Override
public DefaultRequestBodyUriSpec contentType(MediaType contentType) {
getHeaders().setContentType(contentType);
return this;
}
@Override
public DefaultRequestBodyUriSpec contentLength(long contentLength) {
getHeaders().setContentLength(contentLength);
return this;
}
@Override
public DefaultRequestBodyUriSpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
getHeaders().setIfModifiedSince(ifModifiedSince);
return this;
}
@Override
public DefaultRequestBodyUriSpec ifNoneMatch(String... ifNoneMatches) {
getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches));
return this;
}
@Override
public RequestBodySpec attribute(String name, Object value) {
this.attributes.put(name, value);
return this;
}
@Override
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
attributesConsumer.accept(this.attributes);
return this;
}
@Override
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
this.httpRequestConsumer.andThen(requestConsumer) : requestConsumer);
return this;
}
@Override
public RequestBodySpec body(Object body) {
this.body = clientHttpRequest -> writeWithMessageConverters(body, body.getClass(), clientHttpRequest);
return this;
}
@Override
public <T> RequestBodySpec body(T body, ParameterizedTypeReference<T> bodyType) {
this.body = clientHttpRequest -> writeWithMessageConverters(body, bodyType.getType(), clientHttpRequest);
return this;
}
@Override
public RequestBodySpec body(StreamingHttpOutputMessage.Body body) {
this.body = request -> body.writeTo(request.getBody());
return this;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void writeWithMessageConverters(Object body, Type bodyType, ClientHttpRequest clientRequest)
throws IOException {
MediaType contentType = clientRequest.getHeaders().getContentType();
Class<?> bodyClass = body.getClass();
for (HttpMessageConverter messageConverter : DefaultRestClient.this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) {
if (genericMessageConverter.canWrite(bodyType, bodyClass, contentType)) {
logBody(body, contentType, genericMessageConverter);
genericMessageConverter.write(body, bodyType, contentType, clientRequest);
return;
}
}
if (messageConverter.canWrite(bodyClass, contentType)) {
logBody(body, contentType, messageConverter);
messageConverter.write(body, contentType, clientRequest);
return;
}
}
String message = "No HttpMessageConverter for " + bodyClass.getName();
if (contentType != null) {
message += " and content type \"" + contentType + "\"";
}
throw new RestClientException(message);
}
private void logBody(Object body, @Nullable MediaType mediaType, HttpMessageConverter<?> converter) {
if (logger.isDebugEnabled()) {
StringBuilder msg = new StringBuilder("Writing [");
msg.append(body);
msg.append("] ");
if (mediaType != null) {
msg.append("as \"");
msg.append(mediaType);
msg.append("\" ");
}
msg.append("with ");
msg.append(converter.getClass().getName());
logger.debug(msg.toString());
}
}
@Override
public ResponseSpec retrieve() {
return exchangeInternal(DefaultResponseSpec::new, false);
}
@Override
public <T> T exchange(ExchangeFunction<T> exchangeFunction) {
return exchangeInternal(exchangeFunction, true);
}
private <T> T exchangeInternal(ExchangeFunction<T> exchangeFunction, boolean close) {
Assert.notNull(exchangeFunction, "ExchangeFunction must not be null");
ClientHttpResponse clientResponse = null;
URI uri = null;
try {
uri = initUri();
HttpHeaders headers = initHeaders();
ClientHttpRequest clientRequest = createRequest(uri);
clientRequest.getHeaders().addAll(headers);
if (this.body != null) {
this.body.writeTo(clientRequest);
}
if (this.httpRequestConsumer != null) {
this.httpRequestConsumer.accept(clientRequest);
}
clientResponse = clientRequest.execute();
return exchangeFunction.exchange(clientRequest, clientResponse);
}
catch (IOException ex) {
throw createResourceAccessException(uri, this.httpMethod, ex);
}
finally {
if (close && clientResponse != null) {
clientResponse.close();
}
}
}
private URI initUri() {
return (this.uri != null ? this.uri : DefaultRestClient.this.uriBuilderFactory.expand(""));
}
private HttpHeaders initHeaders() {
HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders;
if (CollectionUtils.isEmpty(this.headers)) {
return (defaultHeaders != null ? defaultHeaders : new HttpHeaders());
}
else if (CollectionUtils.isEmpty(defaultHeaders)) {
return this.headers;
}
else {
HttpHeaders result = new HttpHeaders();
result.putAll(defaultHeaders);
result.putAll(this.headers);
return result;
}
}
private ClientHttpRequest createRequest(URI uri) throws IOException {
ClientHttpRequestFactory factory;
if (DefaultRestClient.this.interceptors != null) {
factory = DefaultRestClient.this.interceptingRequestFactory;
if (factory == null) {
factory = new InterceptingClientHttpRequestFactory(DefaultRestClient.this.clientRequestFactory, DefaultRestClient.this.interceptors);
DefaultRestClient.this.interceptingRequestFactory = factory;
}
}
else {
factory = DefaultRestClient.this.clientRequestFactory;
}
ClientHttpRequest request = factory.createRequest(uri, this.httpMethod);
if (DefaultRestClient.this.initializers != null) {
DefaultRestClient.this.initializers.forEach(initializer -> initializer.initialize(request));
}
return request;
}
private static ResourceAccessException createResourceAccessException(URI url, HttpMethod method, IOException ex) {
StringBuilder msg = new StringBuilder("I/O error on ");
msg.append(method.name());
msg.append(" request for \"");
String urlString = url.toString();
int idx = urlString.indexOf('?');
if (idx != -1) {
msg.append(urlString, 0, idx);
}
else {
msg.append(urlString);
}
msg.append("\": ");
msg.append(ex.getMessage());
return new ResourceAccessException(msg.toString(), ex);
}
@FunctionalInterface
private interface InternalBody {
void writeTo(ClientHttpRequest request) throws IOException;
}
}
private class DefaultResponseSpec implements ResponseSpec {
private final HttpRequest clientRequest;
private final ClientHttpResponse clientResponse;
private final List<StatusHandler> statusHandlers = new ArrayList<>(1);
private final int defaultStatusHandlerCount;
DefaultResponseSpec(HttpRequest clientRequest, ClientHttpResponse clientResponse) {
this.clientRequest = clientRequest;
this.clientResponse = clientResponse;
this.statusHandlers.addAll(DefaultRestClient.this.defaultStatusHandlers);
this.statusHandlers.add(StatusHandler.defaultHandler(DefaultRestClient.this.messageConverters));
this.defaultStatusHandlerCount = this.statusHandlers.size();
}
@Override
public ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate, ErrorHandler errorHandler) {
Assert.notNull(statusPredicate, "StatusPredicate must not be null");
Assert.notNull(errorHandler, "ErrorHandler must not be null");
return onStatusInternal(StatusHandler.of(statusPredicate, errorHandler));
}
@Override
public ResponseSpec onStatus(ResponseErrorHandler errorHandler) {
Assert.notNull(errorHandler, "ErrorHandler must not be null");
return onStatusInternal(StatusHandler.fromErrorHandler(errorHandler));
}
private ResponseSpec onStatusInternal(StatusHandler statusHandler) {
Assert.notNull(statusHandler, "StatusHandler must not be null");
int index = this.statusHandlers.size() - this.defaultStatusHandlerCount; // Default handlers always last
this.statusHandlers.add(index, statusHandler);
return this;
}
@Override
public <T> T body(Class<T> bodyType) {
return readWithMessageConverters(bodyType, bodyType);
}
@Override
public <T> T body(ParameterizedTypeReference<T> bodyType) {
Type type = bodyType.getType();
Class<T> bodyClass = bodyClass(type);
return readWithMessageConverters(type, bodyClass);
}
@Override
public <T> ResponseEntity<T> toEntity(Class<T> bodyType) {
return toEntityInternal(bodyType, bodyType);
}
@Override
public <T> ResponseEntity<T> toEntity(ParameterizedTypeReference<T> bodyType) {
Type type = bodyType.getType();
Class<T> bodyClass = bodyClass(type);
return toEntityInternal(type, bodyClass);
}
private <T> ResponseEntity<T> toEntityInternal(Type bodyType, Class<T> bodyClass) {
T body = readWithMessageConverters(bodyType, bodyClass);
try {
return ResponseEntity.status(this.clientResponse.getStatusCode())
.headers(this.clientResponse.getHeaders())
.body(body);
}
catch (IOException ex) {
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
}
}
@Override
public ResponseEntity<Void> toBodilessEntity() {
try (this.clientResponse) {
applyStatusHandlers(this.clientRequest, this.clientResponse);
return ResponseEntity.status(this.clientResponse.getStatusCode())
.headers(this.clientResponse.getHeaders())
.build();
}
catch (IOException ex) {
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
}
}
@SuppressWarnings("unchecked")
private static <T> Class<T> bodyClass(Type type) {
if (type instanceof Class<?> clazz) {
return (Class<T>) clazz;
}
if (type instanceof ParameterizedType parameterizedType &&
parameterizedType.getRawType() instanceof Class<?> rawType) {
return (Class<T>) rawType;
}
return (Class<T>) Object.class;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private <T> T readWithMessageConverters(Type bodyType, Class<T> bodyClass) {
MediaType contentType = getContentType();
try (this.clientResponse) {
applyStatusHandlers(this.clientRequest, this.clientResponse);
for (HttpMessageConverter<?> messageConverter : DefaultRestClient.this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
if (genericHttpMessageConverter.canRead(bodyType, bodyClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
}
return (T) genericHttpMessageConverter.read(bodyType, bodyClass, this.clientResponse);
}
}
if (messageConverter.canRead(bodyClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
}
return (T) messageConverter.read((Class)bodyClass, this.clientResponse);
}
}
throw new UnknownContentTypeException(bodyType, contentType,
this.clientResponse.getStatusCode(), this.clientResponse.getStatusText(),
this.clientResponse.getHeaders(), RestClientUtils.getBody(this.clientResponse));
}
catch (IOException | HttpMessageNotReadableException ex) {
throw new RestClientException("Error while extracting response for type [" +
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", ex);
}
}
private MediaType getContentType() {
MediaType contentType = this.clientResponse.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
return contentType;
}
private void applyStatusHandlers(HttpRequest request, ClientHttpResponse response) throws IOException {
for (StatusHandler handler : this.statusHandlers) {
if (handler.test(response)) {
handler.handle(request, response);
return;
}
}
}
}
}

375
spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java

@ -0,0 +1,375 @@ @@ -0,0 +1,375 @@
/*
* Copyright 2002-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.web.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInitializer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilderFactory;
/**
* Default implementation of {@link RestClient.Builder}.
*
* @author Arjen Poutsma
* @since 6.1
*/
final class DefaultRestClientBuilder implements RestClient.Builder {
private static final boolean httpComponentsClientPresent;
private static final boolean jackson2Present;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
private static final boolean kotlinSerializationJsonPresent;
private static final boolean jackson2SmilePresent;
private static final boolean jackson2CborPresent;
static {
ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader();
httpComponentsClientPresent = ClassUtils.isPresent("org.apache.hc.client5.http.classic.HttpClient", loader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", loader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", loader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", loader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", loader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", loader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader);
}
@Nullable
private String baseUrl;
@Nullable
private Map<String, ?> defaultUriVariables;
@Nullable
private UriBuilderFactory uriBuilderFactory;
@Nullable
private HttpHeaders defaultHeaders;
@Nullable
private Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest;
@Nullable
private List<StatusHandler> statusHandlers;
@Nullable
private ClientHttpRequestFactory requestFactory;
@Nullable
private List<HttpMessageConverter<?>> messageConverters;
@Nullable
private List<ClientHttpRequestInterceptor> interceptors;
@Nullable
private List<ClientHttpRequestInitializer> initializers;
public DefaultRestClientBuilder() {
}
public DefaultRestClientBuilder(DefaultRestClientBuilder other) {
Assert.notNull(other, "Other must not be null");
this.baseUrl = other.baseUrl;
this.defaultUriVariables = (other.defaultUriVariables != null ?
new LinkedHashMap<>(other.defaultUriVariables) : null);
this.uriBuilderFactory = other.uriBuilderFactory;
if (other.defaultHeaders != null) {
this.defaultHeaders = new HttpHeaders();
this.defaultHeaders.putAll(other.defaultHeaders);
}
else {
this.defaultHeaders = null;
}
this.defaultRequest = other.defaultRequest;
this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null);
this.requestFactory = other.requestFactory;
this.messageConverters = (other.messageConverters != null ?
new ArrayList<>(other.messageConverters) : null);
this.interceptors = (other.interceptors != null) ? new ArrayList<>(other.interceptors) : null;
this.initializers = (other.initializers != null) ? new ArrayList<>(other.initializers) : null;
}
public DefaultRestClientBuilder(RestTemplate restTemplate) {
Assert.notNull(restTemplate, "RestTemplate must not be null");
if (restTemplate.getUriTemplateHandler() instanceof UriBuilderFactory builderFactory) {
this.uriBuilderFactory = builderFactory;
}
this.statusHandlers = new ArrayList<>();
this.statusHandlers.add(StatusHandler.fromErrorHandler(restTemplate.getErrorHandler()));
this.requestFactory = restTemplate.getRequestFactory();
this.messageConverters = new ArrayList<>(restTemplate.getMessageConverters());
if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
}
if (!CollectionUtils.isEmpty(restTemplate.getClientHttpRequestInitializers())) {
this.initializers = new ArrayList<>(restTemplate.getClientHttpRequestInitializers());
}
}
@Override
public RestClient.Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
@Override
public RestClient.Builder defaultUriVariables(Map<String, ?> defaultUriVariables) {
this.defaultUriVariables = defaultUriVariables;
return this;
}
@Override
public RestClient.Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory) {
this.uriBuilderFactory = uriBuilderFactory;
return this;
}
@Override
public RestClient.Builder defaultHeader(String header, String... values) {
initHeaders().put(header, Arrays.asList(values));
return this;
}
@Override
public RestClient.Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(initHeaders());
return this;
}
private HttpHeaders initHeaders() {
if (this.defaultHeaders == null) {
this.defaultHeaders = new HttpHeaders();
}
return this.defaultHeaders;
}
@Override
public RestClient.Builder defaultRequest(Consumer<RestClient.RequestHeadersSpec<?>> defaultRequest) {
this.defaultRequest = this.defaultRequest != null ?
this.defaultRequest.andThen(defaultRequest) : defaultRequest;
return this;
}
@Override
public RestClient.Builder defaultStatusHandler(Predicate<HttpStatusCode> statusPredicate, RestClient.ResponseSpec.ErrorHandler errorHandler) {
return defaultStatusHandlerInternal(StatusHandler.of(statusPredicate, errorHandler));
}
@Override
public RestClient.Builder defaultStatusHandler(ResponseErrorHandler errorHandler) {
return defaultStatusHandlerInternal(StatusHandler.fromErrorHandler(errorHandler));
}
private RestClient.Builder defaultStatusHandlerInternal(StatusHandler statusHandler) {
if (this.statusHandlers == null) {
this.statusHandlers = new ArrayList<>();
}
this.statusHandlers.add(statusHandler);
return this;
}
@Override
public RestClient.Builder requestInterceptor(ClientHttpRequestInterceptor interceptor) {
Assert.notNull(interceptor, "Interceptor must not be null");
initInterceptors().add(interceptor);
return this;
}
@Override
public RestClient.Builder requestInterceptors(Consumer<List<ClientHttpRequestInterceptor>> interceptorsConsumer) {
interceptorsConsumer.accept(initInterceptors());
return this;
}
private List<ClientHttpRequestInterceptor> initInterceptors() {
if (this.interceptors == null) {
this.interceptors = new ArrayList<>();
}
return this.interceptors;
}
@Override
public RestClient.Builder requestInitializer(ClientHttpRequestInitializer initializer) {
Assert.notNull(initializer, "Initializer must not be null");
initInitializers().add(initializer);
return this;
}
@Override
public RestClient.Builder requestInitializers(Consumer<List<ClientHttpRequestInitializer>> initializersConsumer) {
initializersConsumer.accept(initInitializers());
return this;
}
private List<ClientHttpRequestInitializer> initInitializers() {
if (this.initializers == null) {
this.initializers = new ArrayList<>();
}
return this.initializers;
}
@Override
public RestClient.Builder requestFactory(ClientHttpRequestFactory requestFactory) {
this.requestFactory = requestFactory;
return this;
}
@Override
public RestClient.Builder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer) {
configurer.accept(initMessageConverters());
return this;
}
@Override
public RestClient.Builder apply(Consumer<RestClient.Builder> builderConsumer) {
builderConsumer.accept(this);
return this;
}
private List<HttpMessageConverter<?>> initMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter(false));
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (kotlinSerializationJsonPresent) {
this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
this.messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
}
if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
}
return this.messageConverters;
}
@Override
public RestClient.Builder clone() {
return new DefaultRestClientBuilder(this);
}
@Override
public RestClient build() {
ClientHttpRequestFactory requestFactory = initRequestFactory();
UriBuilderFactory uriBuilderFactory = initUriBuilderFactory();
HttpHeaders defaultHeaders = copyDefaultHeaders();
List<HttpMessageConverter<?>> messageConverters = (this.messageConverters != null ?
this.messageConverters : initMessageConverters());
return new DefaultRestClient(requestFactory,
this.interceptors, this.initializers, uriBuilderFactory,
defaultHeaders,
this.statusHandlers,
messageConverters,
new DefaultRestClientBuilder(this)
);
}
private ClientHttpRequestFactory initRequestFactory() {
if (this.requestFactory != null) {
return this.requestFactory;
}
else if (httpComponentsClientPresent) {
return new HttpComponentsClientHttpRequestFactory();
}
else {
return new SimpleClientHttpRequestFactory();
}
}
private UriBuilderFactory initUriBuilderFactory() {
if (this.uriBuilderFactory != null) {
return this.uriBuilderFactory;
}
DefaultUriBuilderFactory factory = (this.baseUrl != null ?
new DefaultUriBuilderFactory(this.baseUrl) : new DefaultUriBuilderFactory());
factory.setDefaultUriVariables(this.defaultUriVariables);
return factory;
}
@Nullable
private HttpHeaders copyDefaultHeaders() {
if (this.defaultHeaders != null) {
HttpHeaders copy = new HttpHeaders();
this.defaultHeaders.forEach((key, values) -> copy.put(key, new ArrayList<>(values)));
return HttpHeaders.readOnlyHttpHeaders(copy);
}
else {
return null;
}
}
}

751
spring-web/src/main/java/org/springframework/web/client/RestClient.java

@ -0,0 +1,751 @@ @@ -0,0 +1,751 @@
/*
* Copyright 2002-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.web.client;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInitializer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory;
/**
* Client to perform HTTP requests, exposing a fluent, synchronous API over
* underlying HTTP client libraries such the JDK {@code HttpClient}, Apache
* HttpComponents, and others.
*
* <p>Use static factory methods {@link #create()}, {@link #create(String)},
* or {@link RestClient#builder()} to prepare an instance. To use the same
* configuration as a {@link RestTemplate}, use {@link #create(RestTemplate)} or
* {@link #builder(RestTemplate)}.
*
* <p>For examples with a response body see:
* <ul>
* <li>{@link RequestHeadersSpec#retrieve() retrieve()}
* <li>{@link RequestHeadersSpec#exchange(RequestHeadersSpec.ExchangeFunction) exchange(Function&lt;ClientHttpRequest, T&gt;)}
* </ul>
* <p>For examples with a request body see:
* <ul>
* <li>{@link RequestBodySpec#body(Object) body(Object)}
* <li>{@link RequestBodySpec#body(Object, ParameterizedTypeReference) body(Object, ParameterizedTypeReference)}
* <li>{@link RequestBodySpec#body(StreamingHttpOutputMessage.Body) body(Consumer&lt;OutputStream&gt;}
* </ul>
*
* @author Arjen Poutsma
* @since 6.1
*/
public interface RestClient {
/**
* Start building an HTTP GET request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> get();
/**
* Start building an HTTP HEAD request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> head();
/**
* Start building an HTTP POST request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec post();
/**
* Start building an HTTP PUT request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec put();
/**
* Start building an HTTP PATCH request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec patch();
/**
* Start building an HTTP DELETE request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> delete();
/**
* Start building an HTTP OPTIONS request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> options();
/**
* Start building a request for the given {@code HttpMethod}.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec method(HttpMethod method);
/**
* Return a builder to create a new {@code RestClient} whose settings are
* replicated from the current {@code RestClient}.
*/
Builder mutate();
// Static, factory methods
/**
* Create a new {@code RestClient}.
* @see #create(String)
* @see #builder()
*/
static RestClient create() {
return new DefaultRestClientBuilder().build();
}
/**
* Variant of {@link #create()} that accepts a default base URL. For more
* details see {@link Builder#baseUrl(String) Builder.baseUrl(String)}.
* @param baseUrl the base URI for all requests
* @see #builder()
*/
static RestClient create(String baseUrl) {
return new DefaultRestClientBuilder().baseUrl(baseUrl).build();
}
/**
* Create a new {@code RestClient} based on the configuration of the
* given {@code RestTemplate}. The returned builder is configured with the
* template's
* <ul>
* <li>{@link RestTemplate#getRequestFactory() ClientHttpRequestFactory},</li>
* <li>{@link RestTemplate#getMessageConverters() HttpMessageConverters},</li>
* <li>{@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors},</li>
* <li>{@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers},</li>
* <li>{@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}, and</li>
* <li>{@linkplain RestTemplate#getErrorHandler() error handler}.</li>
* </ul>
* @param restTemplate the rest template to base the returned client's
* configuration on
* @return a {@code RestClient} initialized with the {@code restTemplate}'s
* configuration
*/
static RestClient create(RestTemplate restTemplate) {
return new DefaultRestClientBuilder(restTemplate).build();
}
/**
* Obtain a {@code RestClient} builder.
*/
static RestClient.Builder builder() {
return new DefaultRestClientBuilder();
}
/**
* Obtain a {@code RestClient} builder based on the configuration of the
* given {@code RestTemplate}. The returned builder is configured with the
* template's
* <ul>
* <li>{@link RestTemplate#getRequestFactory() ClientHttpRequestFactory},</li>
* <li>{@link RestTemplate#getMessageConverters() HttpMessageConverters},</li>
* <li>{@link RestTemplate#getInterceptors() ClientHttpRequestInterceptors},</li>
* <li>{@link RestTemplate#getClientHttpRequestInitializers() ClientHttpRequestInitializers},</li>
* <li>{@link RestTemplate#getUriTemplateHandler() UriBuilderFactory}, and</li>
* <li>{@linkplain RestTemplate#getErrorHandler() error handler}.</li>
* </ul>
* @param restTemplate the rest template to base the returned builder's
* configuration on
* @return a {@code RestClient} builder initialized with {@code restTemplate}'s
* configuration
*/
static RestClient.Builder builder(RestTemplate restTemplate) {
return new DefaultRestClientBuilder(restTemplate);
}
/**
* A mutable builder for creating a {@link RestClient}.
*/
interface Builder {
/**
* Configure a base URL for requests. Effectively a shortcut for:
* <p>
* <pre class="code">
* String baseUrl = "https://abc.go.com/v1";
* DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
* RestClient client = RestClient.builder().uriBuilderFactory(factory).build();
* </pre>
* <p>The {@code DefaultUriBuilderFactory} is used to prepare the URL
* for every request with the given base URL, unless the URL request
* for a given URL is absolute in which case the base URL is ignored.
* <p><strong>Note:</strong> this method is mutually exclusive with
* {@link #uriBuilderFactory(UriBuilderFactory)}. If both are used, the
* baseUrl value provided here will be ignored.
* @see DefaultUriBuilderFactory#DefaultUriBuilderFactory(String)
* @see #uriBuilderFactory(UriBuilderFactory)
*/
Builder baseUrl(String baseUrl);
/**
* Configure default URL variable values to use when expanding URI
* templates with a {@link Map}. Effectively a shortcut for:
* <p>
* <pre class="code">
* Map&lt;String, ?&gt; defaultVars = ...;
* DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
* factory.setDefaultVariables(defaultVars);
* RestClient client = RestClient.builder().uriBuilderFactory(factory).build();
* </pre>
* <p><strong>Note:</strong> this method is mutually exclusive with
* {@link #uriBuilderFactory(UriBuilderFactory)}. If both are used, the
* defaultUriVariables value provided here will be ignored.
* @see DefaultUriBuilderFactory#setDefaultUriVariables(Map)
* @see #uriBuilderFactory(UriBuilderFactory)
*/
Builder defaultUriVariables(Map<String, ?> defaultUriVariables);
/**
* Provide a pre-configured {@link UriBuilderFactory} instance. This is
* an alternative to, and effectively overrides the following shortcut
* properties:
* <ul>
* <li>{@link #baseUrl(String)}
* <li>{@link #defaultUriVariables(Map)}.
* </ul>
* @param uriBuilderFactory the URI builder factory to use
* @see #baseUrl(String)
* @see #defaultUriVariables(Map)
*/
Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory);
/**
* Global option to specify a header to be added to every request,
* if the request does not already contain such a header.
* @param header the header name
* @param values the header values
*/
Builder defaultHeader(String header, String... values);
/**
* Provides access to every {@link #defaultHeader(String, String...)}
* declared so far with the possibility to add, replace, or remove.
* @param headersConsumer the consumer
*/
Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer);
/**
* Provide a consumer to customize every request being built.
* @param defaultRequest the consumer to use for modifying requests
*/
Builder defaultRequest(Consumer<RequestHeadersSpec<?>> defaultRequest);
/**
* Register a default
* {@linkplain ResponseSpec#onStatus(Predicate, ResponseSpec.ErrorHandler) status handler}
* to apply to every response. Such default handlers are applied in the
* order in which they are registered, and after any others that are
* registered for a specific response.
* @param statusPredicate to match responses with
* @param errorHandler handler that typically, though not necessarily,
* throws an exception
* @return this builder
*/
Builder defaultStatusHandler(Predicate<HttpStatusCode> statusPredicate,
ResponseSpec.ErrorHandler errorHandler);
/**
* Register a default
* {@linkplain ResponseSpec#onStatus(ResponseErrorHandler) status handler}
* to apply to every response. Such default handlers are applied in the
* order in which they are registered, and after any others that are
* registered for a specific response.
* @param errorHandler handler that typically, though not necessarily,
* throws an exception
* @return this builder
*/
Builder defaultStatusHandler(ResponseErrorHandler errorHandler);
/**
* Add the given request interceptor to the end of the interceptor chain.
* @param interceptor the interceptor to be added to the chain
*/
Builder requestInterceptor(ClientHttpRequestInterceptor interceptor);
/**
* Manipulate the interceptors with the given consumer. The list provided to
* the consumer is "live", so that the consumer can be used to remove
* interceptors, change ordering, etc.
* @param interceptorsConsumer a function that consumes the interceptors list
* @return this builder
*/
Builder requestInterceptors(Consumer<List<ClientHttpRequestInterceptor>> interceptorsConsumer);
/**
* Add the given request initializer to the end of the initializer chain.
* @param initializer the initializer to be added to the chain
*/
Builder requestInitializer(ClientHttpRequestInitializer initializer);
/**
* Manipulate the initializers with the given consumer. The list provided to
* the consumer is "live", so that the consumer can be used to remove
* initializers, change ordering, etc.
* @param initializersConsumer a function that consumes the initializers list
* @return this builder
*/
Builder requestInitializers(Consumer<List<ClientHttpRequestInitializer>> initializersConsumer);
/**
* Configure the {@link ClientHttpRequestFactory} to use. This is useful
* for plugging in and/or customizing options of the underlying HTTP
* client library (e.g. SSL).
* @param requestFactory the request factory to use
*/
Builder requestFactory(ClientHttpRequestFactory requestFactory);
/**
* Configure the message converters for the {@code RestClient} to use.
* @param configurer the configurer to apply
*/
Builder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer);
/**
* Apply the given {@code Consumer} to this builder instance.
* <p>This can be useful for applying pre-packaged customizations.
* @param builderConsumer the consumer to apply
*/
Builder apply(Consumer<Builder> builderConsumer);
/**
* Clone this {@code RestClient.Builder}.
*/
Builder clone();
/**
* Build the {@link RestClient} instance.
*/
RestClient build();
}
/**
* Contract for specifying the URI for a request.
* @param <S> a self reference to the spec type
*/
interface UriSpec<S extends RequestHeadersSpec<?>> {
/**
* Specify the URI using an absolute, fully constructed {@link URI}.
*/
S uri(URI uri);
/**
* Specify the URI for the request using a URI template and URI variables.
* If a {@link UriBuilderFactory} was configured for the client (e.g.
* with a base URI) it will be used to expand the URI template.
*/
S uri(String uri, Object... uriVariables);
/**
* Specify the URI for the request using a URI template and URI variables.
* If a {@link UriBuilderFactory} was configured for the client (e.g.
* with a base URI) it will be used to expand the URI template.
*/
S uri(String uri, Map<String, ?> uriVariables);
/**
* Specify the URI starting with a URI template and finishing off with a
* {@link UriBuilder} created from the template.
*/
S uri(String uri, Function<UriBuilder, URI> uriFunction);
/**
* Specify the URI by through a {@link UriBuilder}.
* @see #uri(String, Function)
*/
S uri(Function<UriBuilder, URI> uriFunction);
}
/**
* Contract for specifying request headers leading up to the exchange.
* @param <S> a self reference to the spec type
*/
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
* specified by the {@code Accept} header.
* @param acceptableMediaTypes the acceptable media types
* @return this builder
*/
S accept(MediaType... acceptableMediaTypes);
/**
* Set the list of acceptable {@linkplain Charset charsets}, as specified
* by the {@code Accept-Charset} header.
* @param acceptableCharsets the acceptable charsets
* @return this builder
*/
S acceptCharset(Charset... acceptableCharsets);
/**
* Set the value of the {@code If-Modified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param ifModifiedSince the new value of the header
* @return this builder
*/
S ifModifiedSince(ZonedDateTime ifModifiedSince);
/**
* Set the values of the {@code If-None-Match} header.
* @param ifNoneMatches the new value of the header
* @return this builder
*/
S ifNoneMatch(String... ifNoneMatches);
/**
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
*/
S header(String headerName, String... headerValues);
/**
* Provides access to every header declared so far with the possibility
* to add, replace, or remove values.
* @param headersConsumer the consumer to provide access to
* @return this builder
*/
S headers(Consumer<HttpHeaders> headersConsumer);
/**
* Set the attribute with the given name to the given value.
* @param name the name of the attribute to add
* @param value the value of the attribute to add
* @return this builder
*/
S attribute(String name, Object value);
/**
* Provides access to every attribute declared so far with the
* possibility to add, replace, or remove values.
* @param attributesConsumer the consumer to provide access to
* @return this builder
*/
S attributes(Consumer<Map<String, Object>> attributesConsumer);
/**
* Callback for access to the {@link ClientHttpRequest} that in turn
* provides access to the native request of the underlying HTTP library.
* This could be useful for setting advanced, per-request options that
* exposed by the underlying library.
* @param requestConsumer a consumer to access the
* {@code ClientHttpRequest} with
* @return {@code ResponseSpec} to specify how to decode the body
*/
S httpRequest(Consumer<ClientHttpRequest> requestConsumer);
/**
* Proceed to declare how to extract the response. For example to extract
* a {@link ResponseEntity} with status, headers, and body:
* <p><pre>
* ResponseEntity&lt;Person&gt; entity = client.get()
* .uri("/persons/1")
* .accept(MediaType.APPLICATION_JSON)
* .retrieve()
* .toEntity(Person.class);
* </pre>
* <p>Or if interested only in the body:
* <p><pre>
* Person person = client.get()
* .uri("/persons/1")
* .accept(MediaType.APPLICATION_JSON)
* .retrieve()
* .body(Person.class);
* </pre>
* <p>By default, 4xx response code result in a
* {@link HttpClientErrorException} and 5xx response codes in a
* {@link HttpServerErrorException}. To customize error handling, use
* {@link ResponseSpec#onStatus(Predicate, ResponseSpec.ErrorHandler) onStatus} handlers.
*/
ResponseSpec retrieve();
/**
* Exchange the {@link ClientHttpResponse} for a type {@code T}. This
* can be useful for advanced scenarios, for example to decode the
* response differently depending on the response status:
* <p><pre>
* Person person = client.get()
* .uri("/people/1")
* .accept(MediaType.APPLICATION_JSON)
* .exchange((request, response) -&gt; {
* if (response.getStatusCode().equals(HttpStatus.OK)) {
* return deserialize(response.getBody());
* }
* else {
* throw new BusinessException();
* }
* });
* </pre>
* <p><strong>Note:</strong> The response is
* {@linkplain ClientHttpResponse#close() closed} after the exchange
* function has been invoked.
* @param exchangeFunction the function to handle the response with
* @param <T> the type the response will be transformed to
* @return the value returned from the exchange function
*/
<T> T exchange(ExchangeFunction<T> exchangeFunction);
/**
* Defines the contract for {@link #exchange(ExchangeFunction)}.
* @param <T> the type the response will be transformed to
*/
@FunctionalInterface
interface ExchangeFunction<T> {
/**
* Exchange the given response into a type {@code T}.
* @param clientRequest the request
* @param clientResponse the response
* @return the exchanged type
* @throws IOException in case of I/O errors
*/
T exchange(HttpRequest clientRequest, ClientHttpResponse clientResponse) throws IOException;
}
}
/**
* Contract for specifying request headers and body leading up to the exchange.
*/
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @see HttpHeaders#setContentLength(long)
*/
RequestBodySpec contentLength(long contentLength);
/**
* Set the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @see HttpHeaders#setContentType(MediaType)
*/
RequestBodySpec contentType(MediaType contentType);
/**
* Set the body of the request to the given {@code Object}.
* For example:
* <p><pre class="code">
* Person person = ... ;
* ResponseEntity&lt;Void&gt; response = client.post()
* .uri("/persons/{id}", id)
* .contentType(MediaType.APPLICATION_JSON)
* .body(person)
* .retrieve()
* .toBodilessEntity();
* </pre>
* @param body the body of the response
* @return the built response
*/
RequestBodySpec body(Object body);
/**
* Set the body of the response to the given {@code Object}. The parameter
* {@code bodyType} is used to capture the generic type.
* @param body the body of the response
* @param bodyType the type of the body, used to capture the generic type
* @return the built response
*/
<T> RequestBodySpec body(T body, ParameterizedTypeReference<T> bodyType);
/**
* Set the body of the response to the given function that writes to
* an {@link OutputStream}.
* @param body a function that takes an {@code OutputStream} and can
* throw an {@code IOException}
* @return the built response
*/
RequestBodySpec body(StreamingHttpOutputMessage.Body body);
}
/**
* Contract for specifying response operations following the exchange.
*/
interface ResponseSpec {
/**
* Provide a function to map specific error status codes to an error
* handler.
* <p>By default, if there are no matching status handlers, responses
* with status codes &gt;= 400 wil throw a
* {@link RestClientResponseException}.
* @param statusPredicate to match responses with
* @param errorHandler handler that typically, though not necessarily,
* throws an exception
* @return this builder
*/
ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate,
ErrorHandler errorHandler);
/**
* Provide a function to map specific error status codes to an error
* handler.
* <p>By default, if there are no matching status handlers, responses
* with status codes &gt;= 400 wil throw a
* {@link RestClientResponseException}.
* @param errorHandler the error handler
* @return this builder
*/
ResponseSpec onStatus(ResponseErrorHandler errorHandler);
/**
* Extract the body as an object of the given type.
* @param bodyType the type of return value
* @param <T> the body type
* @return the body, or {@code null} if no response body was available
* @throws RestClientResponseException by default when receiving a
* response with a status code of 4xx or 5xx. Use
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
* handling.
*/
@Nullable
<T> T body(Class<T> bodyType);
/**
* Extract the body as an object of the given type.
* @param bodyType the type of return value
* @param <T> the body type
* @return the body, or {@code null} if no response body was available
* @throws RestClientResponseException by default when receiving a
* response with a status code of 4xx or 5xx. Use
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
* handling.
*/
@Nullable
<T> T body(ParameterizedTypeReference<T> bodyType);
/**
* Return a {@code ResponseEntity} with the body decoded to an Object of
* the given type.
* @param bodyType the expected response body type
* @param <T> response body type
* @return the {@code ResponseEntity} with the decoded body
* @throws RestClientResponseException by default when receiving a
* response with a status code of 4xx or 5xx. Use
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
* handling.
*/
<T> ResponseEntity<T> toEntity(Class<T> bodyType);
/**
* Return a {@code ResponseEntity} with the body decoded to an Object of
* the given type.
* @param bodyType the expected response body type
* @param <T> response body type
* @return the {@code ResponseEntity} with the decoded body
* @throws RestClientResponseException by default when receiving a
* response with a status code of 4xx or 5xx. Use
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
* handling.
*/
<T> ResponseEntity<T> toEntity(ParameterizedTypeReference<T> bodyType);
/**
* Return a {@code ResponseEntity} without a body.
* @return the {@code ResponseEntity}
* @throws RestClientResponseException by default when receiving a
* response with a status code of 4xx or 5xx. Use
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
* handling.
*/
ResponseEntity<Void> toBodilessEntity();
/**
* Used in {@link #onStatus(Predicate, ErrorHandler)}.
*/
@FunctionalInterface
interface ErrorHandler {
/**
* Handle the error in the given response.
* @param response the response with the error
* @throws IOException in case of I/O errors
*/
void handle(HttpRequest request, ClientHttpResponse response) throws IOException;
}
}
/**
* Contract for specifying request headers and URI for a request.
* @param <S> a self reference to the spec type
*/
interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>> extends UriSpec<S>, RequestHeadersSpec<S> {
}
/**
* Contract for specifying request headers, body and URI for a request.
*/
interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<RequestBodySpec> {
}
}

52
spring-web/src/main/java/org/springframework/web/client/RestClientUtils.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* Copyright 2002-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.web.client;
import java.io.IOException;
import java.nio.charset.Charset;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMessage;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;
/**
* Internal methods shared between types in this package.
*
* @author Arjen Poutsma
* @since 6.1
*/
abstract class RestClientUtils {
public static byte[] getBody(HttpInputMessage message) {
try {
return FileCopyUtils.copyToByteArray(message.getBody());
}
catch (IOException ignore) {
}
return new byte[0];
}
@Nullable
public static Charset getCharset(HttpMessage response) {
HttpHeaders headers = response.getHeaders();
MediaType contentType = headers.getContentType();
return (contentType != null ? contentType.getCharset() : null);
}
}

156
spring-web/src/main/java/org/springframework/web/client/StatusHandler.java

@ -0,0 +1,156 @@ @@ -0,0 +1,156 @@
/*
* Copyright 2002-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.web.client;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.core.ResolvableType;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* Used by {@link DefaultRestClient} and {@link DefaultRestClientBuilder}.
*
* @author Arjen Poutsma
* @since 6.1
*/
final class StatusHandler {
private final ResponsePredicate predicate;
private final RestClient.ResponseSpec.ErrorHandler errorHandler;
private StatusHandler(ResponsePredicate predicate, RestClient.ResponseSpec.ErrorHandler errorHandler) {
this.predicate = predicate;
this.errorHandler = errorHandler;
}
public static StatusHandler of(Predicate<HttpStatusCode> predicate,
RestClient.ResponseSpec.ErrorHandler errorHandler) {
Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(errorHandler, "ErrorHandler must not be null");
return new StatusHandler(response -> predicate.test(response.getStatusCode()), errorHandler);
}
public static StatusHandler fromErrorHandler(ResponseErrorHandler errorHandler) {
Assert.notNull(errorHandler, "ErrorHandler must not be null");
return new StatusHandler(errorHandler::hasError, (request, response) ->
errorHandler.handleError(request.getURI(), request.getMethod(), response));
}
public static StatusHandler defaultHandler(List<HttpMessageConverter<?>> messageConverters) {
return new StatusHandler(response -> response.getStatusCode().isError(),
(request, response) -> {
HttpStatusCode statusCode = response.getStatusCode();
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = RestClientUtils.getBody(response);
Charset charset = RestClientUtils.getCharset(response);
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
RestClientResponseException ex;
if (statusCode.is4xxClientError()) {
ex = HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
}
else if (statusCode.is5xxServerError()) {
ex = HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
}
else {
ex = new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
}
if (!CollectionUtils.isEmpty(messageConverters)) {
ex.setBodyConvertFunction(initBodyConvertFunction(response, body, messageConverters));
}
throw ex;
});
}
private static Function<ResolvableType, ?> initBodyConvertFunction(ClientHttpResponse response, byte[] body, List<HttpMessageConverter<?>> messageConverters) {
Assert.state(!CollectionUtils.isEmpty(messageConverters), "Expected message converters");
return resolvableType -> {
try {
HttpMessageConverterExtractor<?> extractor =
new HttpMessageConverterExtractor<>(resolvableType.getType(), messageConverters);
return extractor.extractData(new ClientHttpResponseDecorator(response) {
@Override
public InputStream getBody() {
return new ByteArrayInputStream(body);
}
});
}
catch (IOException ex) {
throw new RestClientException("Error while extracting response for type [" + resolvableType + "]", ex);
}
};
}
private static String getErrorMessage(int rawStatusCode, String statusText, @Nullable byte[] responseBody,
@Nullable Charset charset) {
String preface = rawStatusCode + " " + statusText + ": ";
if (ObjectUtils.isEmpty(responseBody)) {
return preface + "[no body]";
}
charset = (charset != null ? charset : StandardCharsets.UTF_8);
String bodyText = new String(responseBody, charset);
bodyText = LogFormatUtils.formatValue(bodyText, -1, true);
return preface + bodyText;
}
public boolean test(ClientHttpResponse response) throws IOException {
return this.predicate.test(response);
}
public void handle(HttpRequest request, ClientHttpResponse response) throws IOException {
this.errorHandler.handle(request, response);
}
@FunctionalInterface
private interface ResponsePredicate {
boolean test(ClientHttpResponse response) throws IOException;
}
}

2
spring-web/src/main/java/org/springframework/web/client/package-info.java

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
/**
* Core package of the client-side web support.
* Provides a RestTemplate class and various callback interfaces.
* Provides the RestTemplate and RestClient.
*/
@NonNullApi
@NonNullFields

758
spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java

@ -0,0 +1,758 @@ @@ -0,0 +1,758 @@
/*
* Copyright 2002-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.web.client;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.web.testfixture.xml.Pojo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Named.named;
/**
* Integration tests for {@link RestClient}.
*
* @author Arjen Poutsma
*/
class RestClientIntegrationTests {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("clientHttpRequestFactories")
@interface ParameterizedWebClientTest {
}
static Stream<Named<ClientHttpRequestFactory>> clientHttpRequestFactories() {
return Stream.of(
named("JDK HttpURLConnection", new SimpleClientHttpRequestFactory()),
named("HttpComponents", new HttpComponentsClientHttpRequestFactory()),
named("OkHttp", new OkHttp3ClientHttpRequestFactory()),
named("Jetty", new JettyClientHttpRequestFactory()),
named("JDK HttpClient", new JdkClientHttpRequestFactory())
);
}
private MockWebServer server;
private RestClient restClient;
private void startServer(ClientHttpRequestFactory requestFactory) {
this.server = new MockWebServer();
this.restClient = RestClient
.builder()
.requestFactory(requestFactory)
.baseUrl(this.server.url("/").toString())
.build();
}
@AfterEach
void shutdown() throws IOException {
if (server != null) {
this.server.shutdown();
}
}
@ParameterizedWebClientTest
void retrieve(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response ->
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
String result = this.restClient.get()
.uri("/greeting")
.header("X-Test-Header", "testvalue")
.retrieve()
.body(String.class);
assertThat(result).isEqualTo("Hello Spring!");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getHeader("X-Test-Header")).isEqualTo("testvalue");
assertThat(request.getPath()).isEqualTo("/greeting");
});
}
@ParameterizedWebClientTest
void retrieveJson(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response
.setHeader("Content-Type", "application/json")
.setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
Pojo result = this.restClient.get()
.uri("/pojo")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(Pojo.class);
assertThat(result.getFoo()).isEqualTo("foofoo");
assertThat(result.getBar()).isEqualTo("barbar");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/pojo");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveJsonWithParameterizedTypeReference(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String content = "{\"containerValue\":{\"bar\":\"barbar\",\"foo\":\"foofoo\"}}";
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody(content));
ValueContainer<Pojo> result = this.restClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(new ParameterizedTypeReference<ValueContainer<Pojo>>() {});
assertThat(result.getContainerValue()).isNotNull();
Pojo pojo = result.getContainerValue();
assertThat(pojo.getFoo()).isEqualTo("foofoo");
assertThat(pojo.getBar()).isEqualTo("barbar");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveJsonAsResponseEntity(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody(content));
ResponseEntity<String> result = this.restClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(result.getHeaders().getContentLength()).isEqualTo(31);
assertThat(result.getBody()).isEqualTo(content);
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveJsonAsBodilessEntity(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
ResponseEntity<Void> result = this.restClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toBodilessEntity();
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(result.getHeaders().getContentLength()).isEqualTo(31);
assertThat(result.getBody()).isNull();
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveJsonArray(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response
.setHeader("Content-Type", "application/json")
.setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]"));
List<Pojo> result = this.restClient.get()
.uri("/pojos")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
assertThat(result).hasSize(2);
assertThat(result.get(0).getFoo()).isEqualTo("foo1");
assertThat(result.get(0).getBar()).isEqualTo("bar1");
assertThat(result.get(1).getFoo()).isEqualTo("foo2");
assertThat(result.get(1).getBar()).isEqualTo("bar2");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/pojos");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveJsonArrayAsResponseEntityList(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody(content));
ResponseEntity<List<Pojo>> result = this.restClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(new ParameterizedTypeReference<>() {});
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(result.getHeaders().getContentLength()).isEqualTo(58);
assertThat(result.getBody()).hasSize(2);
assertThat(result.getBody().get(0).getFoo()).isEqualTo("foo1");
assertThat(result.getBody().get(0).getBar()).isEqualTo("bar1");
assertThat(result.getBody().get(1).getFoo()).isEqualTo("foo2");
assertThat(result.getBody().get(1).getBar()).isEqualTo("bar2");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieveJsonAsSerializedText(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody(content));
String result = this.restClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(String.class);
assertThat(result).isEqualTo(content);
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
@SuppressWarnings("rawtypes")
void retrieveJsonNull(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response
.setResponseCode(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("null"));
Map result = this.restClient.get()
.uri("/null")
.retrieve()
.body(Map.class);
assertThat(result).isNull();
}
@ParameterizedWebClientTest
void retrieve404(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(404)
.setHeader("Content-Type", "text/plain"));
assertThatExceptionOfType(HttpClientErrorException.NotFound.class).isThrownBy(() ->
this.restClient.get().uri("/greeting")
.retrieve()
.body(String.class)
);
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
}
@ParameterizedWebClientTest
void retrieve404WithBody(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(404)
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
assertThatExceptionOfType(HttpClientErrorException.NotFound.class).isThrownBy(() ->
this.restClient.get()
.uri("/greeting")
.retrieve()
.body(String.class)
);
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
}
@ParameterizedWebClientTest
void retrieve500(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String errorMessage = "Internal Server error";
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody(errorMessage));
String path = "/greeting";
try {
this.restClient.get()
.uri(path)
.retrieve()
.body(String.class);
}
catch (HttpServerErrorException ex) {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assumeFalse(requestFactory instanceof JdkClientHttpRequestFactory, "JDK HttpClient does not expose status text");
assertThat(ex.getStatusText()).isEqualTo("Server Error");
assertThat(ex.getResponseHeaders().getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
assertThat(ex.getResponseBodyAsString()).isEqualTo(errorMessage);
}
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo(path));
}
@ParameterizedWebClientTest
void retrieve500AsEntity(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
assertThatExceptionOfType(HttpServerErrorException.InternalServerError.class).isThrownBy(() ->
this.restClient.get()
.uri("/").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(String.class)
);
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieve500AsBodilessEntity(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
assertThatExceptionOfType(HttpServerErrorException.InternalServerError.class).isThrownBy(() ->
this.restClient.get()
.uri("/").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toBodilessEntity()
);
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void retrieve555UnknownStatus(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
int errorStatus = 555;
assertThat(HttpStatus.resolve(errorStatus)).isNull();
String errorMessage = "Something went wrong";
prepareResponse(response -> response.setResponseCode(errorStatus)
.setHeader("Content-Type", "text/plain").setBody(errorMessage));
try {
this.restClient.get()
.uri("/unknownPage")
.retrieve()
.body(String.class);
}
catch (HttpServerErrorException ex) {
assumeFalse(requestFactory instanceof JdkClientHttpRequestFactory, "JDK HttpClient does not expose status text");
assertThat(ex.getMessage()).isEqualTo("555 Server Error: \"Something went wrong\"");
assertThat(ex.getStatusText()).isEqualTo("Server Error");
assertThat(ex.getResponseHeaders().getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
assertThat(ex.getResponseBodyAsString()).isEqualTo(errorMessage);
}
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/unknownPage"));
}
@ParameterizedWebClientTest
void postPojoAsJson(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setHeader("Content-Type", "application/json")
.setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"));
Pojo result = this.restClient.post()
.uri("/pojo/capitalize")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body(new Pojo("foofoo", "barbar"))
.retrieve()
.body(Pojo.class);
assertThat(result).isNotNull();
assertThat(result.getFoo()).isEqualTo("FOOFOO");
assertThat(result.getBar()).isEqualTo("BARBAR");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/pojo/capitalize");
assertThat(request.getBody().readUtf8()).isEqualTo("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}");
// assertThat(request.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo("31");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void statusHandler(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
assertThatExceptionOfType(MyException.class).isThrownBy(() ->
this.restClient.get()
.uri("/greeting")
.retrieve()
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new MyException("500 error!");
})
.body(String.class)
);
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
}
@ParameterizedWebClientTest
void statusHandlerParameterizedTypeReference(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
assertThatExceptionOfType(MyException.class).isThrownBy(() ->
this.restClient.get()
.uri("/greeting")
.retrieve()
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new MyException("500 error!");
})
.body(new ParameterizedTypeReference<String>() {
})
);
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
}
@ParameterizedWebClientTest
void statusHandlerSuppressedErrorSignal(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
String result = this.restClient.get()
.uri("/greeting")
.retrieve()
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {})
.body(String.class);
assertThat(result).isEqualTo("Internal Server error");
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
}
@ParameterizedWebClientTest
void statusHandlerSuppressedErrorSignalWithEntity(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String content = "Internal Server error";
prepareResponse(response -> response.setResponseCode(500)
.setHeader("Content-Type", "text/plain").setBody(content));
ResponseEntity<String> result = this.restClient.get()
.uri("/").accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {})
.toEntity(String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(result.getBody()).isEqualTo(content);
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@ParameterizedWebClientTest
void exchangeForPlainText(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setBody("Hello Spring!"));
String result = this.restClient.get()
.uri("/greeting")
.header("X-Test-Header", "testvalue")
.exchange((request, response) -> new String(RestClientUtils.getBody(response), StandardCharsets.UTF_8));
assertThat(result).isEqualTo("Hello Spring!");
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getHeader("X-Test-Header")).isEqualTo("testvalue");
assertThat(request.getPath()).isEqualTo("/greeting");
});
}
@ParameterizedWebClientTest
void exchangeFor404(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setResponseCode(404)
.setHeader("Content-Type", "text/plain").setBody("Not Found"));
String result = this.restClient.get()
.uri("/greeting")
.exchange((request, response) -> new String(RestClientUtils.getBody(response), StandardCharsets.UTF_8));
assertThat(result).isEqualTo("Not Found");
expectRequestCount(1);
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
}
@ParameterizedWebClientTest
void requestInitializer(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setHeader("Content-Type", "text/plain")
.setBody("Hello Spring!"));
RestClient initializedClient = this.restClient.mutate()
.requestInitializer(request -> request.getHeaders().add("foo", "bar"))
.build();
String result = initializedClient.get()
.uri("/greeting")
.retrieve()
.body(String.class);
assertThat(result).isEqualTo("Hello Spring!");
expectRequestCount(1);
expectRequest(request -> assertThat(request.getHeader("foo")).isEqualTo("bar"));
}
@ParameterizedWebClientTest
void requestInterceptor(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
prepareResponse(response -> response.setHeader("Content-Type", "text/plain")
.setBody("Hello Spring!"));
RestClient interceptedClient = this.restClient.mutate()
.requestInterceptor((request, body, execution) -> {
request.getHeaders().add("foo", "bar");
return execution.execute(request, body);
})
.build();
String result = interceptedClient.get()
.uri("/greeting")
.retrieve()
.body(String.class);
assertThat(result).isEqualTo("Hello Spring!");
expectRequestCount(1);
expectRequest(request -> assertThat(request.getHeader("foo")).isEqualTo("bar"));
}
@ParameterizedWebClientTest
void filterForErrorHandling(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {
ClientHttpResponse response = execution.execute(request, body);
List<String> headerValues = response.getHeaders().get("Foo");
if (CollectionUtils.isEmpty(headerValues)) {
throw new MyException("Response does not contain Foo header");
}
else {
return response;
}
};
RestClient interceptedClient = this.restClient.mutate().requestInterceptor(interceptor).build();
// header not present
prepareResponse(response -> response
.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
assertThatExceptionOfType(MyException.class).isThrownBy(() ->
interceptedClient.get()
.uri("/greeting")
.retrieve()
.body(String.class)
);
// header present
prepareResponse(response -> response.setHeader("Content-Type", "text/plain")
.setHeader("Foo", "Bar")
.setBody("Hello Spring!"));
String result = interceptedClient.get()
.uri("/greeting")
.retrieve().body(String.class);
assertThat(result).isEqualTo("Hello Spring!");
expectRequestCount(2);
}
@ParameterizedWebClientTest
void invalidDomain(ClientHttpRequestFactory requestFactory) {
startServer(requestFactory);
String url = "http://example.invalid";
assertThatExceptionOfType(ResourceAccessException.class).isThrownBy(() ->
this.restClient.get().uri(url).retrieve().toBodilessEntity()
);
}
private void prepareResponse(Consumer<MockResponse> consumer) {
MockResponse response = new MockResponse();
consumer.accept(response);
this.server.enqueue(response);
}
private void expectRequest(Consumer<RecordedRequest> consumer) {
try {
consumer.accept(this.server.takeRequest());
}
catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
}
private void expectRequestCount(int count) {
assertThat(this.server.getRequestCount()).isEqualTo(count);
}
@SuppressWarnings("serial")
private static class MyException extends RuntimeException {
MyException(String message) {
super(message);
}
}
static class ValueContainer<T> {
private T containerValue;
public T getContainerValue() {
return containerValue;
}
public void setContainerValue(T containerValue) {
this.containerValue = containerValue;
}
}
}
Loading…
Cancel
Save