Polishing

See gh-30117
This commit is contained in:
Rossen Stoyanchev
2023-07-10 11:24:30 +01:00
committed by rstoyanchev
parent 47667ab990
commit 22376c2efa
21 changed files with 185 additions and 195 deletions

View File

@@ -17,8 +17,9 @@
package org.springframework.web.client.support;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpCookie;
@@ -27,19 +28,17 @@ 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 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}.
*
* <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
@@ -48,11 +47,17 @@ 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 boolean supportsRequestAttributes() {
return false;
}
@Override
public void exchange(HttpRequestValues requestValues) {
this.restTemplate.exchange(newRequest(requestValues), Void.class);
@@ -74,14 +79,10 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
}
@Override
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues,
ParameterizedTypeReference<T> bodyType) {
return this.restTemplate.exchange(newRequest(requestValues), bodyType);
}
public <T> ResponseEntity<T> exchangeForEntity(
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
@Override
public boolean supportsRequestAttributes() {
return false;
return this.restTemplate.exchange(newRequest(requestValues), bodyType);
}
private RequestEntity<?> newRequest(HttpRequestValues requestValues) {
@@ -90,8 +91,9 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
uri = requestValues.getUri();
}
else if (requestValues.getUriTemplate() != null) {
uri = this.restTemplate.getUriTemplateHandler().expand(requestValues.getUriTemplate(),
requestValues.getUriVariables());
String uriTemplate = requestValues.getUriTemplate();
Map<String, String> variables = requestValues.getUriVariables();
uri = this.restTemplate.getUriTemplateHandler().expand(uriTemplate, variables);
}
else {
throw new IllegalStateException("Neither full URL nor URI template");
@@ -100,35 +102,30 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
HttpMethod httpMethod = requestValues.getHttpMethod();
Assert.notNull(httpMethod, "HttpMethod is required");
RequestEntity.BodyBuilder builder = RequestEntity.method(httpMethod, uri)
.headers(requestValues.getHeaders());
RequestEntity.BodyBuilder builder = RequestEntity.method(httpMethod, uri);
builder.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("; ")));
List<String> cookies = new ArrayList<>();
requestValues.getCookies().forEach((name, values) -> values.forEach(value -> {
HttpCookie cookie = new HttpCookie(name, value);
cookies.add(cookie.toString());
}));
builder.header(HttpHeaders.COOKIE, String.join("; ", cookies));
}
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
* Create a {@link RestTemplateAdapter} with the given {@link RestTemplate}.
*/
public static RestTemplateAdapter forTemplate(RestTemplate restTemplate) {
public static RestTemplateAdapter create(RestTemplate restTemplate) {
return new RestTemplateAdapter(restTemplate);
}

View File

@@ -29,10 +29,8 @@ 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.
* Convenient base class for a {@link ReactorHttpExchangeAdapter} implementation
* adapting to the synchronous {@link HttpExchangeAdapter} contract.
*
* @author Rossen Stoyanchev
* @since 6.1
@@ -55,30 +53,21 @@ public abstract class AbstractReactorHttpExchangeAdapter
/**
* Configure the registry for adapting various reactive types.
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
* default settings.
* Configure the {@link ReactiveAdapterRegistry} to use.
* <p>By default, this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry reactiveAdapterRegistry) {
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
}
/**
* Return the configured reactive type registry of adapters.
*/
@Override
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
return this.reactiveAdapterRegistry;
}
/**
* 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.
* Configure how long to block for the response of an HTTP service method
* as described in {@link #getBlockTimeout()}.
*/
public void setBlockTimeout(@Nullable Duration blockTimeout) {
this.blockTimeout = blockTimeout;
@@ -90,6 +79,7 @@ public abstract class AbstractReactorHttpExchangeAdapter
return this.blockTimeout;
}
@Override
public void exchange(HttpRequestValues requestValues) {
if (this.blockTimeout != null) {
@@ -126,7 +116,9 @@ public abstract class AbstractReactorHttpExchangeAdapter
}
@Override
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
public <T> ResponseEntity<T> exchangeForEntity(
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
ResponseEntity<T> entity = (this.blockTimeout != null ?
exchangeForEntityMono(requestValues, bodyType).block(this.blockTimeout) :
exchangeForEntityMono(requestValues, bodyType).block());

View File

@@ -91,59 +91,55 @@ public interface HttpClientAdapter {
/**
* Adapt this {@link HttpClientAdapter} to {@link ReactorHttpExchangeAdapter}.
* @return a {@link ReactorHttpExchangeAdapter} instance created that delegating to
* the underlying {@link HttpClientAdapter} implementation
* Adapt this instance to {@link ReactorHttpExchangeAdapter}.
* @since 6.1
*/
default ReactorHttpExchangeAdapter asHttpExchangeAdapter() {
HttpClientAdapter delegate = this;
default ReactorHttpExchangeAdapter asReactorExchangeAdapter() {
return new AbstractReactorHttpExchangeAdapter() {
@Override
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
return delegate.requestToVoid(requestValues);
public boolean supportsRequestAttributes() {
return true;
}
@Override
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues requestValues) {
return delegate.requestToHeaders(requestValues);
public Mono<Void> exchangeForMono(HttpRequestValues values) {
return HttpClientAdapter.this.requestToVoid(values);
}
@Override
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return delegate.requestToBody(requestValues, bodyType);
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues values) {
return HttpClientAdapter.this.requestToHeaders(values);
}
@Override
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return delegate.requestToBodyFlux(requestValues, bodyType);
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
return HttpClientAdapter.this.requestToBody(values, bodyType);
}
@Override
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues requestValues) {
return delegate.requestToBodilessEntity(requestValues);
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
return HttpClientAdapter.this.requestToBodyFlux(values, bodyType);
}
@Override
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues values) {
return HttpClientAdapter.this.requestToBodilessEntity(values);
}
@Override
public <T> Mono<ResponseEntity<T>> exchangeForEntityMono(
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return delegate.requestToEntity(requestValues, bodyType);
return HttpClientAdapter.this.requestToEntity(requestValues, bodyType);
}
@Override
public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return delegate.requestToEntityFlux(requestValues, bodyType);
}
@Override
public boolean supportsRequestAttributes() {
return true;
return HttpClientAdapter.this.requestToEntityFlux(requestValues, bodyType);
}
};
}

View File

@@ -30,6 +30,11 @@ import org.springframework.lang.Nullable;
*/
public interface HttpExchangeAdapter {
/**
* Whether the underlying client supports use of request attributes.
*/
boolean supportsRequestAttributes();
/**
* Perform the given request, and release the response content, if any.
* @param requestValues the request to perform
@@ -49,7 +54,7 @@ public interface HttpExchangeAdapter {
* @param requestValues the request to perform
* @param bodyType the target type to decode to
* @param <T> the type the response is decoded to
* @return the decoded response.
* @return the decoded response body.
*/
@Nullable
<T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
@@ -57,18 +62,15 @@ public interface HttpExchangeAdapter {
/**
* Variant of {@link #exchange(HttpRequestValues)} with additional
* access to the response status and headers.
* @return the response entity with status and headers.
*/
ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues);
/**
* Variant of {@link #exchangeForBody(HttpRequestValues, ParameterizedTypeReference)}
* with additional access to the response status and headers.
* @return the response entity with status, headers, and body.
*/
<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

@@ -80,7 +80,10 @@ final class HttpServiceMethod {
this.method = method;
this.parameters = initMethodParameters(method);
this.argumentResolvers = argumentResolvers;
this.requestValuesInitializer = HttpRequestValuesInitializer.create(method, containingClass, embeddedValueResolver);
this.requestValuesInitializer =
HttpRequestValuesInitializer.create(method, containingClass, embeddedValueResolver);
this.responseFunction =
(REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter reactorAdapter ?
ReactorExchangeResponseFunction.create(reactorAdapter, method) :
@@ -291,52 +294,54 @@ final class HttpServiceMethod {
return this.responseFunction.apply(requestValues);
}
/**
* Create the {@code ResponseFunction} that matches the method return type.
*/
public static ResponseFunction create(HttpExchangeAdapter client, Method method) {
if (KotlinDetector.isSuspendingFunction(method)) {
throw new IllegalStateException("Kotlin Coroutines are only supported with reactive implementations");
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();
MethodParameter param = new MethodParameter(method, -1).nestedIfOptional();
Class<?> paramType = param.getNestedParameterType();
Function<HttpRequestValues, Object> responseFunction;
if (actualReturnType.equals(void.class) || actualReturnType.equals(Void.class)) {
if (paramType.equals(void.class) || paramType.equals(Void.class)) {
responseFunction = requestValues -> {
client.exchange(requestValues);
return null;
};
}
else if (actualReturnType.equals(HttpHeaders.class)) {
responseFunction = request -> processResponse(client.exchangeForHeaders(request),
returnOptional);
else if (paramType.equals(HttpHeaders.class)) {
responseFunction = request -> asOptionalIfNecessary(client.exchangeForHeaders(request), param);
}
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 if (paramType.equals(ResponseEntity.class)) {
MethodParameter bodyParam = param.nested();
if (bodyParam.getNestedParameterType().equals(Void.class)) {
responseFunction = request ->
asOptionalIfNecessary(client.exchangeForBodilessEntity(request), param);
}
else {
ParameterizedTypeReference<?> bodyTypeReference = ParameterizedTypeReference
.forType(bodyParam.getNestedGenericParameterType());
responseFunction = request -> processResponse(client.exchangeForEntity(request,
bodyTypeReference), returnOptional);
ParameterizedTypeReference<?> bodyTypeRef =
ParameterizedTypeReference.forType(bodyParam.getNestedGenericParameterType());
responseFunction = request ->
asOptionalIfNecessary(client.exchangeForEntity(request, bodyTypeRef), param);
}
}
else {
ParameterizedTypeReference<?> bodyTypeReference = ParameterizedTypeReference
.forType(actualReturnParam.getNestedGenericParameterType());
responseFunction = request -> processResponse(client.exchangeForBody(request,
bodyTypeReference), returnOptional);
ParameterizedTypeReference<?> bodyTypeRef =
ParameterizedTypeReference.forType(param.getNestedGenericParameterType());
responseFunction = request ->
asOptionalIfNecessary(client.exchangeForBody(request, bodyTypeRef), param);
}
return new ExchangeResponseFunction(responseFunction);
}
private static @Nullable Object processResponse(@Nullable Object response,
boolean returnOptional) {
return returnOptional ? Optional.ofNullable(response) : response;
private static @Nullable Object asOptionalIfNecessary(@Nullable Object response, MethodParameter param) {
return param.getParameterType().equals(Optional.class) ? Optional.ofNullable(response) : response;
}
}
@@ -372,7 +377,7 @@ final class HttpServiceMethod {
/**
* Create the {@code ResponseFunction} that matches the method's return type.
* Create the {@code ResponseFunction} that matches the method return type.
*/
public static ResponseFunction create(ReactorHttpExchangeAdapter client, Method method) {
MethodParameter returnParam = new MethodParameter(method, -1);

View File

@@ -119,7 +119,7 @@ public final class HttpServiceProxyFactory {
@SuppressWarnings("removal")
@Deprecated(since = "6.1", forRemoval = true)
public static Builder builder(HttpClientAdapter clientAdapter) {
return new Builder().exchangeAdapter(clientAdapter.asHttpExchangeAdapter());
return new Builder().exchangeAdapter(clientAdapter.asReactorExchangeAdapter());
}
/**
@@ -169,7 +169,7 @@ public final class HttpServiceProxyFactory {
@SuppressWarnings("removal")
@Deprecated(since = "6.1", forRemoval = true)
public Builder clientAdapter(HttpClientAdapter clientAdapter) {
this.exchangeAdapter = clientAdapter.asHttpExchangeAdapter();
this.exchangeAdapter = clientAdapter.asReactorExchangeAdapter();
return this;
}
@@ -262,12 +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));
if (this.exchangeAdapter.supportsRequestAttributes()) {
resolvers.add(new RequestAttributeArgumentResolver());
}
// Specific type
resolvers.add(new UrlArgumentResolver());

View File

@@ -37,19 +37,18 @@ import org.springframework.lang.Nullable;
public interface ReactorHttpExchangeAdapter extends HttpExchangeAdapter {
/**
* Return the configured reactive type registry of adapters.
* Return the configured {@link ReactiveAdapterRegistry}.
*/
ReactiveAdapterRegistry getReactiveAdapterRegistry();
/**
* Return the configured time to block for the response of an HTTP service method with
* a synchronous (blocking) method signature.
* Return the configured time to block for the response from 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.
* <p>By default, 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();