Add RestTemplate support for HTTP interface client
See gh-30117
This commit is contained in:
committed by
rstoyanchev
parent
bf82ed7186
commit
268f3c853e
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user