Add RestTemplate support for HTTP interface client

See gh-30117
This commit is contained in:
Olga MaciaszekSharma
2023-07-06 19:07:40 +02:00
committed by rstoyanchev
parent bf82ed7186
commit 268f3c853e
21 changed files with 1272 additions and 227 deletions

View File

@@ -0,0 +1,136 @@
/*
* 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.support;
import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.service.invoker.HttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
/**
* An {@link HttpExchangeAdapter} that enables an {@link HttpServiceProxyFactory} to use
* {@link RestTemplate} for request execution.
* <p>
* Use static factory methods in this class to create an {@link HttpServiceProxyFactory}
* configured with a given {@link RestTemplate}.
*
* @author Olga Maciaszek-Sharma
* @since 6.1
*/
public final class RestTemplateAdapter implements HttpExchangeAdapter {
private final RestTemplate restTemplate;
// Private constructor; use static factory methods to instantiate
private RestTemplateAdapter(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public Void exchange(HttpRequestValues requestValues) {
this.restTemplate.exchange(newRequest(requestValues), Void.class);
return null;
}
@Override
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
return this.restTemplate.exchange(newRequest(requestValues), Void.class).getHeaders();
}
@Override
public <T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return this.restTemplate.exchange(newRequest(requestValues), bodyType).getBody();
}
@Override
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
return this.restTemplate.exchange(newRequest(requestValues), Void.class);
}
@Override
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues,
ParameterizedTypeReference<T> bodyType) {
return this.restTemplate.exchange(newRequest(requestValues), bodyType);
}
@Override
public boolean supportsRequestAttributes() {
return false;
}
private RequestEntity<?> newRequest(HttpRequestValues requestValues) {
URI uri;
if (requestValues.getUri() != null) {
uri = requestValues.getUri();
}
else if (requestValues.getUriTemplate() != null) {
uri = this.restTemplate.getUriTemplateHandler().expand(requestValues.getUriTemplate(),
requestValues.getUriVariables());
}
else {
throw new IllegalStateException("Neither full URL nor URI template");
}
HttpMethod httpMethod = requestValues.getHttpMethod();
Assert.notNull(httpMethod, "HttpMethod is required");
RequestEntity.BodyBuilder builder = RequestEntity.method(httpMethod, uri)
.headers(requestValues.getHeaders());
if (!requestValues.getCookies().isEmpty()) {
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
requestValues.getCookies()
.forEach((name, values) -> values.forEach(value ->
cookies.add(name, new HttpCookie(name, value))));
builder.header(HttpHeaders.COOKIE,
cookies.values()
.stream()
.flatMap(List::stream)
.map(HttpCookie::toString)
.collect(Collectors.joining("; ")));
}
if (requestValues.getBodyValue() != null) {
return builder.body(requestValues.getBodyValue());
}
return builder.build();
}
/**
* Create a {@link RestTemplateAdapter} for the given {@link RestTemplate} instance.
* @param restTemplate the {@link RestTemplate} to use
* @return the created adapter instance
*/
public static RestTemplateAdapter forTemplate(RestTemplate restTemplate) {
return new RestTemplateAdapter(restTemplate);
}
}

View File

@@ -29,6 +29,10 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A base reactive adapter that implements both {@link HttpClientAdapter}
* and {@link HttpExchangeAdapter}. Allows to ensure backwards compatibility
* with the deprecated {@link HttpClientAdapter} and handles blocking from reactive
* publishers to objects where necessary.
*
* @author Rossen Stoyanchev
* @since 6.1
@@ -51,16 +55,16 @@ public abstract class AbstractReactorHttpExchangeAdapter
/**
*
* @param reactiveAdapterRegistry
* Configure the registry for adapting various reactive types.
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
* default settings.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry reactiveAdapterRegistry) {
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
}
/**
*
* @return
* Return the configured reactive type registry of adapters.
*/
@Override
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
@@ -68,31 +72,31 @@ public abstract class AbstractReactorHttpExchangeAdapter
}
/**
*
* @param blockTimeout
* Configure how long to block for the response of an HTTP service method with a
* synchronous (blocking) method signature.
* <p>
* By default, this is not set, in which case the behavior depends on connection and
* request timeout settings of the underlying HTTP client. We recommend configuring
* timeout values directly on the underlying HTTP client, which provides more
* control over such settings.
*/
public void setBlockTimeout(@Nullable Duration blockTimeout) {
this.blockTimeout = blockTimeout;
}
/**
*
* @return
*/
@Override
@Nullable
public Duration getBlockTimeout() {
return this.blockTimeout;
}
@Override
public void exchange(HttpRequestValues requestValues) {
public Void exchange(HttpRequestValues requestValues) {
if (this.blockTimeout != null) {
exchangeForMono(requestValues).block(this.blockTimeout);
return exchangeForMono(requestValues).block(this.blockTimeout);
}
else {
exchangeForMono(requestValues).block();
return exchangeForMono(requestValues).block();
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.http.ResponseEntity;
* {@linkplain HttpServiceProxyFactory#createClient(Class) HTTP service proxy}.
*
* @author Rossen Stoyanchev
* @author Olga Maciaszek-Sharma
* @since 6.0
* @deprecated in favor of {@link ReactorHttpExchangeAdapter}
*/
@@ -91,50 +92,58 @@ public interface HttpClientAdapter {
/**
* Adapt this {@link HttpClientAdapter} to {@link ReactorHttpExchangeAdapter}.
* @return
* @return a {@link ReactorHttpExchangeAdapter} instance created that delegating to
* the underlying {@link HttpClientAdapter} implementation
* @since 6.1
*/
default ReactorHttpExchangeAdapter asHttpExchangeAdapter() {
HttpClientAdapter delegate = this;
return new AbstractReactorHttpExchangeAdapter() {
@Override
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
return requestToVoid(requestValues);
return delegate.requestToVoid(requestValues);
}
@Override
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues requestValues) {
return requestToHeaders(requestValues);
return delegate.requestToHeaders(requestValues);
}
@Override
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return requestToBody(requestValues, bodyType);
return delegate.requestToBody(requestValues, bodyType);
}
@Override
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return requestToBodyFlux(requestValues, bodyType);
return delegate.requestToBodyFlux(requestValues, bodyType);
}
@Override
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues requestValues) {
return requestToBodilessEntity(requestValues);
return delegate.requestToBodilessEntity(requestValues);
}
@Override
public <T> Mono<ResponseEntity<T>> exchangeForEntityMono(
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return requestToEntity(requestValues, bodyType);
return delegate.requestToEntity(requestValues, bodyType);
}
@Override
public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return requestToEntityFlux(requestValues, bodyType);
return delegate.requestToEntityFlux(requestValues, bodyType);
}
@Override
public boolean supportsRequestAttributes() {
return true;
}
};
}

View File

@@ -34,7 +34,7 @@ public interface HttpExchangeAdapter {
* Perform the given request, and release the response content, if any.
* @param requestValues the request to perform
*/
void exchange(HttpRequestValues requestValues);
Void exchange(HttpRequestValues requestValues);
/**
* Perform the given request, release the response content, and return the
@@ -48,8 +48,8 @@ public interface HttpExchangeAdapter {
* Perform the given request and decode the response content to the given type.
* @param requestValues the request to perform
* @param bodyType the target type to decode to
* @return the decoded response.
* @param <T> the type the response is decoded to
* @return the decoded response.
*/
@Nullable
<T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
@@ -66,4 +66,9 @@ public interface HttpExchangeAdapter {
*/
<T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
/**
* A flag that indicates whether request attributes are supported by a specific client
* adapter.
*/
boolean supportsRequestAttributes();
}

View File

@@ -53,6 +53,7 @@ import org.springframework.web.service.annotation.HttpExchange;
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @author Olga Maciaszek-Sharma
* @since 6.0
*/
final class HttpServiceMethod {
@@ -282,17 +283,57 @@ final class HttpServiceMethod {
}
private record ExchangeResponseFunction(
Function<HttpRequestValues, Object> responseFunction) implements ResponseFunction {
@Override
public Object execute(HttpRequestValues requestValues) {
return null;
return this.responseFunction.apply(requestValues);
}
public static ResponseFunction create(HttpExchangeAdapter client, Method method) {
return new ExchangeResponseFunction(httpRequestValues -> null);
if (KotlinDetector.isSuspendingFunction(method)) {
throw new IllegalStateException("Kotlin Coroutines are only supported with reactive implementations");
}
MethodParameter actualReturnParam = new MethodParameter(method, -1).nestedIfOptional();
boolean returnOptional = actualReturnParam.getParameterType().equals(Optional.class);
Class<?> actualReturnType = actualReturnParam.getNestedParameterType();
Function<HttpRequestValues, Object> responseFunction;
if (actualReturnType.equals(void.class) || actualReturnType.equals(Void.class)) {
responseFunction = client::exchange;
}
else if (actualReturnType.equals(HttpHeaders.class)) {
responseFunction = request -> processResponse(client.exchangeForHeaders(request),
returnOptional);
}
else if (actualReturnType.equals(ResponseEntity.class)) {
MethodParameter bodyParam = actualReturnParam.nested();
Class<?> bodyType = bodyParam.getNestedParameterType();
if (bodyType.equals(Void.class)) {
responseFunction = request -> processResponse(client
.exchangeForBodilessEntity(request), returnOptional);
}
else {
ParameterizedTypeReference<?> bodyTypeReference = ParameterizedTypeReference
.forType(bodyParam.getNestedGenericParameterType());
responseFunction = request -> processResponse(client.exchangeForEntity(request,
bodyTypeReference), returnOptional);
}
}
else {
ParameterizedTypeReference<?> bodyTypeReference = ParameterizedTypeReference
.forType(actualReturnParam.getNestedGenericParameterType());
responseFunction = request -> processResponse(client.exchangeForBody(request,
bodyTypeReference), returnOptional);
}
return new ExchangeResponseFunction(responseFunction);
}
private static @Nullable Object processResponse(@Nullable Object response,
boolean returnOptional) {
return returnOptional ? Optional.ofNullable(response) : response;
}
}

View File

@@ -249,6 +249,7 @@ public final class HttpServiceProxyFactory {
this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver);
}
@SuppressWarnings("DataFlowIssue")
private List<HttpServiceArgumentResolver> initArgumentResolvers() {
// Custom
@@ -261,10 +262,12 @@ public final class HttpServiceProxyFactory {
resolvers.add(new RequestHeaderArgumentResolver(service));
resolvers.add(new RequestBodyArgumentResolver());
resolvers.add(new PathVariableArgumentResolver(service));
if (this.exchangeAdapter.supportsRequestAttributes()) {
resolvers.add(new RequestAttributeArgumentResolver());
}
resolvers.add(new RequestParamArgumentResolver(service));
resolvers.add(new RequestPartArgumentResolver());
resolvers.add(new CookieValueArgumentResolver(service));
resolvers.add(new RequestAttributeArgumentResolver());
// Specific type
resolvers.add(new UrlArgumentResolver());

View File

@@ -25,6 +25,7 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
/**
* Contract to abstract a Project Reactor, HTTP client to decouple it from the
@@ -36,17 +37,23 @@ import org.springframework.http.ResponseEntity;
public interface ReactorHttpExchangeAdapter extends HttpExchangeAdapter {
/**
*
* @return
* Return the configured reactive type registry of adapters.
*/
ReactiveAdapterRegistry getReactiveAdapterRegistry();
/**
* Return the configured time to block for the response of an HTTP service method with
* a synchronous (blocking) method signature.
*
* @return
* <p>
* By default, this is not set, in which case the behavior depends on connection and
* request timeout settings of the underlying HTTP client. We recommend configuring
* timeout values directly on the underlying HTTP client, which provides more *
* control over such settings.
*/
@Nullable
Duration getBlockTimeout();
/**
* Perform the given request, and release the response content, if any.
* @param requestValues the request to perform