Polishing
See gh-30117
This commit is contained in:
committed by
rstoyanchev
parent
47667ab990
commit
22376c2efa
@@ -388,7 +388,8 @@ either using `WebClient`:
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder().exchangeAdapter(WebClientAdapter.forClient(webClient)).build();
|
||||
WebClientAdapter adapter = WebClientAdapter.forClient(webClient)
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
@@ -398,8 +399,9 @@ or using `RestTemplate`:
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder().exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate)).build();
|
||||
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.forTemplate(restTemplate);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -51,31 +51,27 @@ import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy} using
|
||||
* {@link RestTemplate} and {@link MockWebServer}.
|
||||
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy}
|
||||
* with {@link RestTemplateAdapter} connecting to {@link MockWebServer}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class RestTemplateHttpServiceProxyTests {
|
||||
class RestTemplateAdapterTests {
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private TestService testService;
|
||||
private Service service;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.server = new MockWebServer();
|
||||
prepareResponse();
|
||||
this.testService = initTestService();
|
||||
}
|
||||
|
||||
private TestService initTestService() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(this.server.url("/").toString()));
|
||||
return HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate))
|
||||
.build()
|
||||
.createClient(TestService.class);
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
|
||||
this.service = HttpServiceProxyFactory.builder().exchangeAdapter(adapter).build().createClient(Service.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@@ -86,32 +82,33 @@ class RestTemplateHttpServiceProxyTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void getRequest() throws InterruptedException {
|
||||
String response = testService.getRequest();
|
||||
void greeting() throws InterruptedException {
|
||||
String response = this.service.getGreeting();
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(response).isEqualTo("Hello Spring!");
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/test");
|
||||
assertThat(request.getPath()).isEqualTo("/greeting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRequestWithPathVariable() throws InterruptedException {
|
||||
ResponseEntity<String> response = testService.getRequestWithPathVariable("456");
|
||||
void greetingById() throws InterruptedException {
|
||||
ResponseEntity<String> response = this.service.getGreetingById("456");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("Hello Spring!");
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/test/456");
|
||||
assertThat(request.getPath()).isEqualTo("/greeting/456");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRequestWithDynamicUri() throws InterruptedException {
|
||||
void greetingWithDynamicUri() throws InterruptedException {
|
||||
URI dynamicUri = this.server.url("/greeting/123").uri();
|
||||
|
||||
Optional<String> response = testService.getRequestWithDynamicUri(dynamicUri, "456");
|
||||
Optional<String> response = this.service.getGreetingWithDynamicUri(dynamicUri, "456");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(response.orElse("empty")).isEqualTo("Hello Spring!");
|
||||
@@ -120,12 +117,12 @@ class RestTemplateHttpServiceProxyTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void postWithRequestHeader() throws InterruptedException {
|
||||
testService.postRequestWithHeader("testHeader", "testBody");
|
||||
void postWithHeader() throws InterruptedException {
|
||||
service.postWithHeader("testHeader", "testBody");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
assertThat(request.getPath()).isEqualTo("/test");
|
||||
assertThat(request.getPath()).isEqualTo("/greeting");
|
||||
assertThat(request.getHeaders().get("testHeaderName")).isEqualTo("testHeader");
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("testBody");
|
||||
}
|
||||
@@ -136,7 +133,7 @@ class RestTemplateHttpServiceProxyTests {
|
||||
map.add("param1", "value 1");
|
||||
map.add("param2", "value 2");
|
||||
|
||||
testService.postForm(map);
|
||||
service.postForm(map);
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeaders().get("Content-Type"))
|
||||
@@ -151,7 +148,7 @@ class RestTemplateHttpServiceProxyTests {
|
||||
MultipartFile file = new MockMultipartFile(fileName, originalFileName, MediaType.APPLICATION_JSON_VALUE,
|
||||
"test".getBytes());
|
||||
|
||||
testService.postMultipart(file, "test2");
|
||||
service.postMultipart(file, "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeaders().get("Content-Type")).startsWith("multipart/form-data;boundary=");
|
||||
@@ -163,8 +160,8 @@ class RestTemplateHttpServiceProxyTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void putRequestWithCookies() throws InterruptedException {
|
||||
testService.putRequestWithCookies("test1", "test2");
|
||||
void putWithCookies() throws InterruptedException {
|
||||
service.putWithCookies("test1", "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("PUT");
|
||||
@@ -172,8 +169,8 @@ class RestTemplateHttpServiceProxyTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void putRequestWithSameNameCookies() throws InterruptedException {
|
||||
testService.putRequestWithSameNameCookies("test1", "test2");
|
||||
void putWithSameNameCookies() throws InterruptedException {
|
||||
service.putWithSameNameCookies("test1", "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("PUT");
|
||||
@@ -186,20 +183,21 @@ class RestTemplateHttpServiceProxyTests {
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
private interface TestService {
|
||||
|
||||
@GetExchange("/test")
|
||||
String getRequest();
|
||||
private interface Service {
|
||||
|
||||
@GetExchange("/test/{id}")
|
||||
ResponseEntity<String> getRequestWithPathVariable(@PathVariable String id);
|
||||
@GetExchange("/greeting")
|
||||
String getGreeting();
|
||||
|
||||
@GetExchange("/test/{id}")
|
||||
Optional<String> getRequestWithDynamicUri(@Nullable URI uri, @PathVariable String id);
|
||||
@GetExchange("/greeting/{id}")
|
||||
ResponseEntity<String> getGreetingById(@PathVariable String id);
|
||||
|
||||
@PostExchange("/test")
|
||||
void postRequestWithHeader(@RequestHeader("testHeaderName") String testHeader,
|
||||
@RequestBody String requestBody);
|
||||
@GetExchange("/greeting/{id}")
|
||||
Optional<String> getGreetingWithDynamicUri(@Nullable URI uri, @PathVariable String id);
|
||||
|
||||
@PostExchange("/greeting")
|
||||
void postWithHeader(
|
||||
@RequestHeader("testHeaderName") String testHeader, @RequestBody String requestBody);
|
||||
|
||||
@PostExchange(contentType = "application/x-www-form-urlencoded")
|
||||
void postForm(@RequestParam MultiValueMap<String, String> params);
|
||||
@@ -208,12 +206,12 @@ class RestTemplateHttpServiceProxyTests {
|
||||
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
|
||||
|
||||
@PutExchange
|
||||
void putRequestWithCookies(@CookieValue String firstCookie,
|
||||
@CookieValue String secondCookie);
|
||||
void putWithCookies(
|
||||
@CookieValue String firstCookie, @CookieValue String secondCookie);
|
||||
|
||||
@PutExchange
|
||||
void putRequestWithSameNameCookies(@CookieValue("testCookie") String firstCookie,
|
||||
@CookieValue("testCookie") String secondCookie);
|
||||
void putWithSameNameCookies(
|
||||
@CookieValue("testCookie") String firstCookie, @CookieValue("testCookie") String secondCookie);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -63,7 +63,7 @@ class KotlinRestTemplateHttpServiceProxyTests {
|
||||
val restTemplate = RestTemplate()
|
||||
restTemplate.uriTemplateHandler = DefaultUriBuilderFactory(server.url("/").toString())
|
||||
return HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate))
|
||||
.exchangeAdapter(RestTemplateAdapter.create(restTemplate))
|
||||
.build()
|
||||
.createClient(TestService::class.java)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsRequestAttributes() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
|
||||
return newRequest(requestValues).retrieve().toBodilessEntity().then();
|
||||
@@ -132,8 +137,4 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
|
||||
return new WebClientAdapter(webClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRequestAttributes() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy}
|
||||
* using {@link WebClient} and {@link MockWebServer}.
|
||||
* with {@link WebClientAdapter} connecting to {@link MockWebServer}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class WebClientHttpServiceProxyTests {
|
||||
public class WebClientAdapterTests {
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
@@ -82,7 +82,7 @@ public class WebClientHttpServiceProxyTests {
|
||||
prepareResponse(response ->
|
||||
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
StepVerifier.create(initHttpService().getGreeting())
|
||||
StepVerifier.create(initService().getGreeting())
|
||||
.expectNext("Hello Spring!")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
@@ -103,7 +103,7 @@ public class WebClientHttpServiceProxyTests {
|
||||
prepareResponse(response ->
|
||||
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
|
||||
|
||||
StepVerifier.create(initHttpService(webClient).getGreetingWithAttribute("myAttributeValue"))
|
||||
StepVerifier.create(initService(webClient).getGreetingWithAttribute("myAttributeValue"))
|
||||
.expectNext("Hello Spring!")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(5));
|
||||
@@ -117,7 +117,7 @@ public class WebClientHttpServiceProxyTests {
|
||||
prepareResponse(response -> response.setResponseCode(200).setBody(expectedBody));
|
||||
|
||||
URI dynamicUri = this.server.url("/greeting/123").uri();
|
||||
String actualBody = initHttpService().getGreetingById(dynamicUri, "456");
|
||||
String actualBody = initService().getGreetingById(dynamicUri, "456");
|
||||
|
||||
assertThat(actualBody).isEqualTo(expectedBody);
|
||||
assertThat(this.server.takeRequest().getRequestUrl().uri()).isEqualTo(dynamicUri);
|
||||
@@ -131,7 +131,7 @@ public class WebClientHttpServiceProxyTests {
|
||||
map.add("param1", "value 1");
|
||||
map.add("param2", "value 2");
|
||||
|
||||
initHttpService().postForm(map);
|
||||
initService().postForm(map);
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeaders().get("Content-Type")).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8");
|
||||
@@ -146,7 +146,7 @@ public class WebClientHttpServiceProxyTests {
|
||||
MultipartFile file = new MockMultipartFile(fileName, originalFileName,
|
||||
MediaType.APPLICATION_JSON_VALUE, "test".getBytes());
|
||||
|
||||
initHttpService().postMultipart(file, "test2");
|
||||
initService().postMultipart(file, "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeaders().get("Content-Type")).startsWith("multipart/form-data;boundary=");
|
||||
@@ -157,14 +157,14 @@ public class WebClientHttpServiceProxyTests {
|
||||
"Content-Type: text/plain;charset=UTF-8", "Content-Length: 5", "test2");
|
||||
}
|
||||
|
||||
private TestHttpService initHttpService() {
|
||||
private Service initService() {
|
||||
WebClient webClient = WebClient.builder().baseUrl(this.server.url("/").toString()).build();
|
||||
return initHttpService(webClient);
|
||||
return initService(webClient);
|
||||
}
|
||||
|
||||
private TestHttpService initHttpService(WebClient webClient) {
|
||||
private Service initService(WebClient webClient) {
|
||||
WebClientAdapter adapter = WebClientAdapter.forClient(webClient);
|
||||
return HttpServiceProxyFactory.builderFor(adapter).build().createClient(TestHttpService.class);
|
||||
return HttpServiceProxyFactory.builderFor(adapter).build().createClient(Service.class);
|
||||
}
|
||||
|
||||
private void prepareResponse(Consumer<MockResponse> consumer) {
|
||||
@@ -174,7 +174,7 @@ public class WebClientHttpServiceProxyTests {
|
||||
}
|
||||
|
||||
|
||||
private interface TestHttpService {
|
||||
private interface Service {
|
||||
|
||||
@GetExchange("/greeting")
|
||||
Mono<String> getGreeting();
|
||||
@@ -128,10 +128,8 @@ class KotlinWebClientHttpServiceProxyTests {
|
||||
}
|
||||
|
||||
private fun initHttpService(webClient: WebClient): TestHttpService {
|
||||
return HttpServiceProxyFactory.builder()
|
||||
.clientAdapter(WebClientAdapter.forClient(webClient))
|
||||
.build()
|
||||
.createClient()
|
||||
val adapter = WebClientAdapter.forClient(webClient)
|
||||
return HttpServiceProxyFactory.builderFor(adapter).build().createClient()
|
||||
}
|
||||
|
||||
private fun prepareResponse(consumer: Consumer<MockResponse>) {
|
||||
|
||||
Reference in New Issue
Block a user