diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc
index f74abe6f29..0828d205fd 100644
--- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc
+++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc
@@ -382,12 +382,24 @@ One, declare an interface with `@HttpExchange` methods:
}
----
-Two, create a proxy that will perform the declared HTTP exchanges:
+Two, create a proxy that will perform the declared HTTP exchanges,
+either using `WebClient`:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
- HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
+ HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder().exchangeAdapter(WebClientAdapter.forClient(webClient)).build();
+
+ RepositoryService service = factory.createClient(RepositoryService.class);
+----
+
+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();
RepositoryService service = factory.createClient(RepositoryService.class);
----
diff --git a/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java b/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java
new file mode 100644
index 0000000000..946006c080
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java
@@ -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.
+ *
+ * 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 exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
+ return this.restTemplate.exchange(newRequest(requestValues), bodyType).getBody();
+ }
+
+ @Override
+ public ResponseEntity exchangeForBodilessEntity(HttpRequestValues requestValues) {
+ return this.restTemplate.exchange(newRequest(requestValues), Void.class);
+ }
+
+ @Override
+ public ResponseEntity exchangeForEntity(HttpRequestValues requestValues,
+ ParameterizedTypeReference 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 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);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractReactorHttpExchangeAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractReactorHttpExchangeAdapter.java
index b675107ded..7bf5541ed9 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractReactorHttpExchangeAdapter.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractReactorHttpExchangeAdapter.java
@@ -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.
+ * 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.
+ *
+ * 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();
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java
index a7bb035bff..1b5aaf587b 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java
@@ -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 exchangeForMono(HttpRequestValues requestValues) {
- return requestToVoid(requestValues);
+ return delegate.requestToVoid(requestValues);
}
@Override
public Mono exchangeForHeadersMono(HttpRequestValues requestValues) {
- return requestToHeaders(requestValues);
+ return delegate.requestToHeaders(requestValues);
}
@Override
public Mono exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- return requestToBody(requestValues, bodyType);
+ return delegate.requestToBody(requestValues, bodyType);
}
@Override
public Flux exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- return requestToBodyFlux(requestValues, bodyType);
+ return delegate.requestToBodyFlux(requestValues, bodyType);
}
@Override
public Mono> exchangeForBodilessEntityMono(HttpRequestValues requestValues) {
- return requestToBodilessEntity(requestValues);
+ return delegate.requestToBodilessEntity(requestValues);
}
@Override
public Mono> exchangeForEntityMono(
HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- return requestToEntity(requestValues, bodyType);
+ return delegate.requestToEntity(requestValues, bodyType);
}
@Override
public Mono>> exchangeForEntityFlux(
HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- return requestToEntityFlux(requestValues, bodyType);
+ return delegate.requestToEntityFlux(requestValues, bodyType);
+ }
+
+ @Override
+ public boolean supportsRequestAttributes() {
+ return true;
}
};
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java
index 67b169fc3b..cd354d87c1 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java
@@ -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 the type the response is decoded to
+ * @return the decoded response.
*/
@Nullable
T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType);
@@ -66,4 +66,9 @@ public interface HttpExchangeAdapter {
*/
ResponseEntity exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference bodyType);
+ /**
+ * A flag that indicates whether request attributes are supported by a specific client
+ * adapter.
+ */
+ boolean supportsRequestAttributes();
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
index 1895f8416d..42b2f7515d 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
@@ -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 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 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;
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
index ff9ebf8572..bbf179307a 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
@@ -249,6 +249,7 @@ public final class HttpServiceProxyFactory {
this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver);
}
+ @SuppressWarnings("DataFlowIssue")
private List 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());
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java
index 7cd1bd0d3a..bad465b1f4 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java
@@ -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
+ *
+ * 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
diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestTemplateHttpServiceProxyTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestTemplateHttpServiceProxyTests.java
new file mode 100644
index 0000000000..b8c382382f
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/client/support/RestTemplateHttpServiceProxyTests.java
@@ -0,0 +1,220 @@
+/*
+ * 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.io.IOException;
+import java.net.URI;
+import java.util.Optional;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.Nullable;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.CookieValue;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.service.annotation.GetExchange;
+import org.springframework.web.service.annotation.PostExchange;
+import org.springframework.web.service.annotation.PutExchange;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+import org.springframework.web.testfixture.servlet.MockMultipartFile;
+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}.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+class RestTemplateHttpServiceProxyTests {
+
+ private MockWebServer server;
+
+ private TestService testService;
+
+ @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);
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @AfterEach
+ void shutDown() throws IOException {
+ if (this.server != null) {
+ this.server.shutdown();
+ }
+ }
+
+ @Test
+ void getRequest() throws InterruptedException {
+ String response = testService.getRequest();
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(response).isEqualTo("Hello Spring!");
+ assertThat(request.getMethod()).isEqualTo("GET");
+ assertThat(request.getPath()).isEqualTo("/test");
+ }
+
+ @Test
+ void getRequestWithPathVariable() throws InterruptedException {
+ ResponseEntity response = testService.getRequestWithPathVariable("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");
+ }
+
+ @Test
+ void getRequestWithDynamicUri() throws InterruptedException {
+ URI dynamicUri = this.server.url("/greeting/123").uri();
+
+ Optional response = testService.getRequestWithDynamicUri(dynamicUri, "456");
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(response.orElse("empty")).isEqualTo("Hello Spring!");
+ assertThat(request.getMethod()).isEqualTo("GET");
+ assertThat(request.getRequestUrl().uri()).isEqualTo(dynamicUri);
+ }
+
+ @Test
+ void postWithRequestHeader() throws InterruptedException {
+ testService.postRequestWithHeader("testHeader", "testBody");
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(request.getMethod()).isEqualTo("POST");
+ assertThat(request.getPath()).isEqualTo("/test");
+ assertThat(request.getHeaders().get("testHeaderName")).isEqualTo("testHeader");
+ assertThat(request.getBody().readUtf8()).isEqualTo("testBody");
+ }
+
+ @Test
+ void formData() throws Exception {
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("param1", "value 1");
+ map.add("param2", "value 2");
+
+ testService.postForm(map);
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(request.getHeaders().get("Content-Type"))
+ .isEqualTo("application/x-www-form-urlencoded;charset=UTF-8");
+ assertThat(request.getBody().readUtf8()).isEqualTo("param1=value+1¶m2=value+2");
+ }
+
+ @Test // gh-30342
+ void multipart() throws InterruptedException {
+ String fileName = "testFileName";
+ String originalFileName = "originalTestFileName";
+ MultipartFile file = new MockMultipartFile(fileName, originalFileName, MediaType.APPLICATION_JSON_VALUE,
+ "test".getBytes());
+
+ testService.postMultipart(file, "test2");
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(request.getHeaders().get("Content-Type")).startsWith("multipart/form-data;boundary=");
+ assertThat(request.getBody().readUtf8()).containsSubsequence(
+ "Content-Disposition: form-data; name=\"file\"; filename=\"originalTestFileName\"",
+ "Content-Type: application/json", "Content-Length: 4", "test",
+ "Content-Disposition: form-data; name=\"anotherPart\"", "Content-Type: text/plain;charset=UTF-8",
+ "Content-Length: 5", "test2");
+ }
+
+ @Test
+ void putRequestWithCookies() throws InterruptedException {
+ testService.putRequestWithCookies("test1", "test2");
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(request.getMethod()).isEqualTo("PUT");
+ assertThat(request.getHeader("Cookie")).isEqualTo("firstCookie=test1; secondCookie=test2");
+ }
+
+ @Test
+ void putRequestWithSameNameCookies() throws InterruptedException {
+ testService.putRequestWithSameNameCookies("test1", "test2");
+
+ RecordedRequest request = this.server.takeRequest();
+ assertThat(request.getMethod()).isEqualTo("PUT");
+ assertThat(request.getHeader("Cookie")).isEqualTo("testCookie=test1; testCookie=test2");
+ }
+
+ private void prepareResponse() {
+ MockResponse response = new MockResponse();
+ response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!");
+ this.server.enqueue(response);
+ }
+
+ private interface TestService {
+
+ @GetExchange("/test")
+ String getRequest();
+
+ @GetExchange("/test/{id}")
+ ResponseEntity getRequestWithPathVariable(@PathVariable String id);
+
+ @GetExchange("/test/{id}")
+ Optional getRequestWithDynamicUri(@Nullable URI uri, @PathVariable String id);
+
+ @PostExchange("/test")
+ void postRequestWithHeader(@RequestHeader("testHeaderName") String testHeader,
+ @RequestBody String requestBody);
+
+ @PostExchange(contentType = "application/x-www-form-urlencoded")
+ void postForm(@RequestParam MultiValueMap params);
+
+ @PostExchange
+ void postMultipart(MultipartFile file, @RequestPart String anotherPart);
+
+ @PutExchange
+ void putRequestWithCookies(@CookieValue String firstCookie,
+ @CookieValue String secondCookie);
+
+ @PutExchange
+ void putRequestWithSameNameCookies(@CookieValue("testCookie") String firstCookie,
+ @CookieValue("testCookie") String secondCookie);
+
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpClientServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpClientServiceMethodTests.java
new file mode 100644
index 0000000000..3aa49de377
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpClientServiceMethodTests.java
@@ -0,0 +1,39 @@
+/*
+ * 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.service.invoker;
+
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Tests for {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter} that
+ * stubs the client invocations.
+ *
+ * The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
+ * {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
+ * typed interface without the need for class casts.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+public class HttpClientServiceMethodTests extends ReactiveHttpServiceMethodTests {
+
+ @BeforeEach
+ void setUp(){
+ this.client = new TestHttpClientAdapter();
+ this.proxyFactory = HttpServiceProxyFactory.builder((HttpClientAdapter) this.client).build();
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpExchangeAdapterServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpExchangeAdapterServiceMethodTests.java
new file mode 100644
index 0000000000..eb3e539a0b
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpExchangeAdapterServiceMethodTests.java
@@ -0,0 +1,41 @@
+/*
+ * 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.service.invoker;
+
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Tests for {@link HttpServiceMethod} with a blocking test {@link TestHttpExchangeAdapter} that
+ * stubs the client invocations.
+ *
+ * The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
+ * {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
+ * typed interface without the need for class casts.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+class HttpExchangeAdapterServiceMethodTests extends HttpServiceMethodTests {
+
+ @BeforeEach
+ void setUp() {
+ this.client = new TestHttpExchangeAdapter();
+ this.proxyFactory = HttpServiceProxyFactory.builder()
+ .exchangeAdapter((HttpExchangeAdapter) this.client)
+ .build();
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java
index 43b0d8c4c0..60e2b54b57 100644
--- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java
@@ -16,18 +16,12 @@
package org.springframework.web.service.invoker;
+import java.util.List;
import java.util.Optional;
-import io.reactivex.rxjava3.core.Completable;
-import io.reactivex.rxjava3.core.Flowable;
-import io.reactivex.rxjava3.core.Single;
import org.junit.jupiter.api.Test;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-import reactor.test.StepVerifier;
import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
@@ -42,84 +36,25 @@ import static org.springframework.http.MediaType.APPLICATION_CBOR_VALUE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
- * Tests for {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
- * that stubs the client invocations.
+ * Base class for testing {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
+ * and a test {@link TestHttpExchangeAdapter} that stub the client invocations.
*
- *
The tests do not create or invoke {@code HttpServiceMethod} directly but
- * rather use {@link HttpServiceProxyFactory} to create a service proxy in order to
- * use a strongly typed interface without the need for class casts.
+ *
+ * The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
+ * {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
+ * typed interface without the need for class casts.
*
* @author Rossen Stoyanchev
+ * @author Olga Maciaszek-Sharma
*/
-class HttpServiceMethodTests {
+abstract class HttpServiceMethodTests {
- private static final ParameterizedTypeReference BODY_TYPE = new ParameterizedTypeReference<>() {};
+ protected static final ParameterizedTypeReference BODY_TYPE = new ParameterizedTypeReference<>() {
+ };
- private final TestHttpClientAdapter client = new TestHttpClientAdapter();
+ protected TestAdapter client;
- private final HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.client).build();
-
-
- @Test
- void reactorService() {
- ReactorService service = this.proxyFactory.createClient(ReactorService.class);
-
- Mono voidMono = service.execute();
- StepVerifier.create(voidMono).verifyComplete();
- verifyClientInvocation("requestToVoid", null);
-
- Mono headersMono = service.getHeaders();
- StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
- verifyClientInvocation("requestToHeaders", null);
-
- Mono body = service.getBody();
- StepVerifier.create(body).expectNext("requestToBody").verifyComplete();
- verifyClientInvocation("requestToBody", BODY_TYPE);
-
- Flux fluxBody = service.getFluxBody();
- StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
- verifyClientInvocation("requestToBodyFlux", BODY_TYPE);
-
- Mono> voidEntity = service.getVoidEntity();
- StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
- verifyClientInvocation("requestToBodilessEntity", null);
-
- Mono> entity = service.getEntity();
- StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
- verifyClientInvocation("requestToEntity", BODY_TYPE);
-
- Mono>> fluxEntity= service.getFluxEntity();
- StepVerifier.create(fluxEntity.flatMapMany(HttpEntity::getBody)).expectNext("request", "To", "Entity", "Flux").verifyComplete();
- verifyClientInvocation("requestToEntityFlux", BODY_TYPE);
-
- assertThat(service.getDefaultMethodValue()).isEqualTo("default value");
- }
-
- @Test
- void rxJavaService() {
- RxJavaService service = this.proxyFactory.createClient(RxJavaService.class);
- Completable completable = service.execute();
- assertThat(completable).isNotNull();
-
- Single headersSingle = service.getHeaders();
- assertThat(headersSingle.blockingGet()).isNotNull();
-
- Single bodySingle = service.getBody();
- assertThat(bodySingle.blockingGet()).isEqualTo("requestToBody");
-
- Flowable bodyFlow = service.getFlowableBody();
- assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
-
- Single> voidEntity = service.getVoidEntity();
- assertThat(voidEntity.blockingGet().getBody()).isNull();
-
- Single> entitySingle = service.getEntity();
- assertThat(entitySingle.blockingGet().getBody()).isEqualTo("requestToEntity");
-
- Single>> entityFlow = service.getFlowableEntity();
- Flowable body = (entityFlow.blockingGet()).getBody();
- assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
- }
+ protected HttpServiceProxyFactory proxyFactory;
@Test
void blockingService() {
@@ -131,16 +66,19 @@ class HttpServiceMethodTests {
assertThat(headers).isNotNull();
String body = service.getBody();
- assertThat(body).isEqualTo("requestToBody");
+ assertThat(body).isEqualTo(client.getInvokedMethodReference());
Optional optional = service.getBodyOptional();
- assertThat(optional).contains("requestToBody");
+ assertThat(optional).contains("body");
ResponseEntity entity = service.getEntity();
- assertThat(entity.getBody()).isEqualTo("requestToEntity");
+ assertThat(entity.getBody()).isEqualTo("entity");
ResponseEntity voidEntity = service.getVoidEntity();
assertThat(voidEntity.getBody()).isNull();
+
+ List list = service.getList();
+ assertThat(list.get(0)).isEqualTo("body");
}
@Test
@@ -166,9 +104,12 @@ class HttpServiceMethodTests {
@Test
void typeAndMethodAnnotatedService() {
- HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.client)
- .embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
- .build();
+ HttpExchangeAdapter actualClient = this.client instanceof HttpClientAdapter httpClient
+ ? httpClient.asHttpExchangeAdapter() : (HttpExchangeAdapter) client;
+ HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder()
+ .exchangeAdapter(actualClient)
+ .embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
+ .build();
MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class);
@@ -189,68 +130,11 @@ class HttpServiceMethodTests {
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
}
- private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference> expectedBodyType) {
- assertThat(this.client.getInvokedMethodName()).isEqualTo(methodName);
+ protected void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference> expectedBodyType) {
+ assertThat(this.client.getInvokedMethodReference()).isEqualTo(methodName);
assertThat(this.client.getBodyType()).isEqualTo(expectedBodyType);
}
-
- @SuppressWarnings("unused")
- private interface ReactorService {
-
- @GetExchange
- Mono execute();
-
- @GetExchange
- Mono getHeaders();
-
- @GetExchange
- Mono getBody();
-
- @GetExchange
- Flux getFluxBody();
-
- @GetExchange
- Mono> getVoidEntity();
-
- @GetExchange
- Mono> getEntity();
-
- @GetExchange
- Mono>> getFluxEntity();
-
- default String getDefaultMethodValue() {
- return "default value";
- }
- }
-
-
- @SuppressWarnings("unused")
- private interface RxJavaService {
-
- @GetExchange
- Completable execute();
-
- @GetExchange
- Single getHeaders();
-
- @GetExchange
- Single getBody();
-
- @GetExchange
- Flowable getFlowableBody();
-
- @GetExchange
- Single> getVoidEntity();
-
- @GetExchange
- Single> getEntity();
-
- @GetExchange
- Single>> getFlowableEntity();
- }
-
-
@SuppressWarnings("unused")
private interface BlockingService {
@@ -271,8 +155,11 @@ class HttpServiceMethodTests {
@GetExchange
ResponseEntity getEntity();
- }
+ @GetExchange
+ List getList();
+
+ }
@SuppressWarnings("unused")
private interface MethodLevelAnnotatedService {
@@ -285,10 +172,10 @@ class HttpServiceMethodTests {
}
-
@SuppressWarnings("unused")
@HttpExchange(url = "${baseUrl}", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE)
private interface TypeAndMethodLevelAnnotatedService extends MethodLevelAnnotatedService {
+
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/ReactiveHttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/ReactiveHttpServiceMethodTests.java
new file mode 100644
index 0000000000..6b2ea90408
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/ReactiveHttpServiceMethodTests.java
@@ -0,0 +1,167 @@
+/*
+ * 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.service.invoker;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.Single;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.service.annotation.GetExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Base class for testing reactive scenarios in {@link HttpServiceMethod} with
+ * a test {@link TestHttpClientAdapter} and a test {@link TestHttpExchangeAdapter}
+ * that stub the client invocations.
+ *
+ *
+ * The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
+ * {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
+ * typed interface without the need for class casts.
+ *
+ * @author Rossen Stoyanchev
+ * @author Olga Maciaszek-Sharma
+ */
+abstract class ReactiveHttpServiceMethodTests extends HttpServiceMethodTests {
+
+ @Test
+ void reactorService() {
+ ReactorService service = proxyFactory.createClient(ReactorService.class);
+
+ Mono voidMono = service.execute();
+ StepVerifier.create(voidMono).verifyComplete();
+ verifyClientInvocation("void", null);
+
+ Mono headersMono = service.getHeaders();
+ StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
+ verifyClientInvocation("headers", null);
+
+ Mono body = service.getBody();
+ StepVerifier.create(body).expectNext("body").verifyComplete();
+ verifyClientInvocation("body", BODY_TYPE);
+
+ Flux fluxBody = service.getFluxBody();
+ StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
+ verifyClientInvocation("bodyFlux", BODY_TYPE);
+
+ Mono> voidEntity = service.getVoidEntity();
+ StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
+ verifyClientInvocation("bodilessEntity", null);
+
+ Mono> entity = service.getEntity();
+ StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
+ verifyClientInvocation("entity", BODY_TYPE);
+
+ Mono>> fluxEntity = service.getFluxEntity();
+ StepVerifier.create(fluxEntity.flatMapMany(HttpEntity::getBody))
+ .expectNext("request", "To", "Entity", "Flux")
+ .verifyComplete();
+ verifyClientInvocation("entityFlux", BODY_TYPE);
+
+ assertThat(service.getDefaultMethodValue()).isEqualTo("default value");
+ }
+
+ @Test
+ void rxJavaService() {
+ RxJavaService service = this.proxyFactory.createClient(RxJavaService.class);
+ Completable completable = service.execute();
+ assertThat(completable).isNotNull();
+
+ Single headersSingle = service.getHeaders();
+ assertThat(headersSingle.blockingGet()).isNotNull();
+
+ Single bodySingle = service.getBody();
+ assertThat(bodySingle.blockingGet()).isEqualTo("body");
+
+ Flowable bodyFlow = service.getFlowableBody();
+ assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
+
+ Single> voidEntity = service.getVoidEntity();
+ assertThat(voidEntity.blockingGet().getBody()).isNull();
+
+ Single> entitySingle = service.getEntity();
+ assertThat(entitySingle.blockingGet().getBody()).isEqualTo("entity");
+
+ Single>> entityFlow = service.getFlowableEntity();
+ Flowable body = (entityFlow.blockingGet()).getBody();
+ assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
+ }
+
+ private interface ReactorService {
+
+ @GetExchange
+ Mono execute();
+
+ @GetExchange
+ Mono getHeaders();
+
+ @GetExchange
+ Mono getBody();
+
+ @GetExchange
+ Flux getFluxBody();
+
+ @GetExchange
+ Mono> getVoidEntity();
+
+ @GetExchange
+ Mono> getEntity();
+
+ @GetExchange
+ Mono>> getFluxEntity();
+
+ default String getDefaultMethodValue() {
+ return "default value";
+ }
+
+ }
+
+ @SuppressWarnings("unused")
+ private interface RxJavaService {
+
+ @GetExchange
+ Completable execute();
+
+ @GetExchange
+ Single getHeaders();
+
+ @GetExchange
+ Single getBody();
+
+ @GetExchange
+ Flowable getFlowableBody();
+
+ @GetExchange
+ Single> getVoidEntity();
+
+ @GetExchange
+ Single> getEntity();
+
+ @GetExchange
+ Single>> getFlowableEntity();
+
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/ReactorExchangeAdapterHttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/ReactorExchangeAdapterHttpServiceMethodTests.java
new file mode 100644
index 0000000000..c9a5b8d552
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/ReactorExchangeAdapterHttpServiceMethodTests.java
@@ -0,0 +1,41 @@
+/*
+ * 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.service.invoker;
+
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Tests for {@link HttpServiceMethod} with an {@link HttpExchangeAdapter}
+ * build from a test {@link TestHttpClientAdapter} that stubs the client invocations.
+ *
+ * The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
+ * {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
+ * typed interface without the need for class casts.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+public class ReactorExchangeAdapterHttpServiceMethodTests extends ReactiveHttpServiceMethodTests {
+
+ @BeforeEach
+ void setUp() {
+ this.client = new TestHttpClientAdapter();
+ this.proxyFactory = HttpServiceProxyFactory.builder()
+ .exchangeAdapter(((HttpClientAdapter) this.client).asHttpExchangeAdapter())
+ .build();
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/TestAdapter.java b/spring-web/src/test/java/org/springframework/web/service/invoker/TestAdapter.java
new file mode 100644
index 0000000000..eb9ab78cd5
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/TestAdapter.java
@@ -0,0 +1,37 @@
+/*
+ * 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.service.invoker;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.lang.Nullable;
+
+/**
+ * A helper interface for verifying method invoked on {@link HttpExchangeAdapter}
+ * and {@link HttpClientAdapter}, as well as their values.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+interface TestAdapter {
+
+ String getInvokedMethodReference();
+
+ HttpRequestValues getRequestValues();
+
+ @Nullable
+ ParameterizedTypeReference> getBodyType();
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java
index 9f7f176766..70eadec4d0 100644
--- a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java
@@ -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.
@@ -16,6 +16,8 @@
package org.springframework.web.service.invoker;
+import java.util.Collections;
+
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -33,10 +35,10 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Olga Maciaszek-Sharma
*/
@SuppressWarnings("unchecked")
-class TestHttpClientAdapter implements HttpClientAdapter {
+class TestHttpClientAdapter implements HttpClientAdapter, TestAdapter {
@Nullable
- private String invokedMethodName;
+ private String invokedForReturnMethodReference;
@Nullable
private HttpRequestValues requestValues;
@@ -44,17 +46,19 @@ class TestHttpClientAdapter implements HttpClientAdapter {
@Nullable
private ParameterizedTypeReference> bodyType;
-
- public String getInvokedMethodName() {
- assertThat(this.invokedMethodName).isNotNull();
- return this.invokedMethodName;
+ @Override
+ public String getInvokedMethodReference() {
+ assertThat(this.invokedForReturnMethodReference).isNotNull();
+ return this.invokedForReturnMethodReference;
}
+ @Override
public HttpRequestValues getRequestValues() {
assertThat(this.requestValues).isNotNull();
return this.requestValues;
}
+ @Override
@Nullable
public ParameterizedTypeReference> getBodyType() {
return this.bodyType;
@@ -65,31 +69,33 @@ class TestHttpClientAdapter implements HttpClientAdapter {
@Override
public Mono requestToVoid(HttpRequestValues requestValues) {
- saveInput("requestToVoid", requestValues, null);
+ saveInput("void", requestValues, null);
return Mono.empty();
}
@Override
public Mono requestToHeaders(HttpRequestValues requestValues) {
- saveInput("requestToHeaders", requestValues, null);
+ saveInput("headers", requestValues, null);
return Mono.just(new HttpHeaders());
}
@Override
public Mono requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- saveInput("requestToBody", requestValues, bodyType);
- return (Mono) Mono.just(getInvokedMethodName());
+ saveInput("body", requestValues, bodyType);
+ return bodyType.getType().getTypeName().contains("List") ?
+ (Mono) Mono.just(Collections.singletonList(getInvokedMethodReference()))
+ : (Mono) Mono.just(getInvokedMethodReference());
}
@Override
public Flux requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- saveInput("requestToBodyFlux", requestValues, bodyType);
+ saveInput("bodyFlux", requestValues, bodyType);
return (Flux) Flux.just("request", "To", "Body", "Flux");
}
@Override
public Mono> requestToBodilessEntity(HttpRequestValues requestValues) {
- saveInput("requestToBodilessEntity", requestValues, null);
+ saveInput("bodilessEntity", requestValues, null);
return Mono.just(ResponseEntity.ok().build());
}
@@ -97,22 +103,22 @@ class TestHttpClientAdapter implements HttpClientAdapter {
public Mono> requestToEntity(
HttpRequestValues requestValues, ParameterizedTypeReference type) {
- saveInput("requestToEntity", requestValues, type);
- return Mono.just((ResponseEntity) ResponseEntity.ok("requestToEntity"));
+ saveInput("entity", requestValues, type);
+ return Mono.just((ResponseEntity) ResponseEntity.ok("entity"));
}
@Override
public Mono>> requestToEntityFlux(
HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
- saveInput("requestToEntityFlux", requestValues, bodyType);
+ saveInput("entityFlux", requestValues, bodyType);
return Mono.just(ResponseEntity.ok((Flux) Flux.just("request", "To", "Entity", "Flux")));
}
private void saveInput(
- String methodName, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference bodyType) {
+ String reference, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference bodyType) {
- this.invokedMethodName = methodName;
+ this.invokedForReturnMethodReference = reference;
this.requestValues = requestValues;
this.bodyType = bodyType;
}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpExchangeAdapter.java b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpExchangeAdapter.java
new file mode 100644
index 0000000000..cf112152b3
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpExchangeAdapter.java
@@ -0,0 +1,106 @@
+/*
+ * 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.service.invoker;
+
+import java.util.Collections;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.Nullable;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link HttpExchangeAdapter} with stubbed responses.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+@SuppressWarnings("unchecked")
+public class TestHttpExchangeAdapter implements HttpExchangeAdapter, TestAdapter {
+
+ @Nullable
+ private String invokedMethodName;
+
+ @Nullable
+ private HttpRequestValues requestValues;
+
+ @Nullable
+ private ParameterizedTypeReference> bodyType;
+
+ public String getInvokedMethodReference() {
+ assertThat(this.invokedMethodName).isNotNull();
+ return this.invokedMethodName;
+ }
+
+ public HttpRequestValues getRequestValues() {
+ assertThat(this.requestValues).isNotNull();
+ return this.requestValues;
+ }
+
+ @Override
+ @Nullable
+ public ParameterizedTypeReference> getBodyType() {
+ return this.bodyType;
+ }
+
+ @Override
+ public Void exchange(HttpRequestValues requestValues) {
+ saveInput("void", requestValues, null);
+ return null;
+ }
+
+ @Override
+ public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
+ saveInput("headers", requestValues, null);
+ return new HttpHeaders();
+ }
+
+ @Override
+ public T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) {
+ saveInput("body", requestValues, bodyType);
+ return bodyType.getType().getTypeName().contains("List")
+ ? (T) Collections.singletonList(getInvokedMethodReference()) : (T) getInvokedMethodReference();
+ }
+
+ @Override
+ public ResponseEntity exchangeForBodilessEntity(HttpRequestValues requestValues) {
+ saveInput("bodilessEntity", requestValues, null);
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ public ResponseEntity exchangeForEntity(HttpRequestValues requestValues,
+ ParameterizedTypeReference bodyType) {
+ saveInput("entity", requestValues, bodyType);
+ return (ResponseEntity) ResponseEntity.ok(this.getInvokedMethodReference());
+ }
+
+ @Override
+ public boolean supportsRequestAttributes() {
+ return false;
+ }
+
+ private void saveInput(String methodName, HttpRequestValues requestValues,
+ @Nullable ParameterizedTypeReference bodyType) {
+
+ this.invokedMethodName = methodName;
+ this.requestValues = requestValues;
+ this.bodyType = bodyType;
+ }
+
+}
diff --git a/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt b/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt
new file mode 100644
index 0000000000..8d3e36f38a
--- /dev/null
+++ b/spring-web/src/test/kotlin/org/springframework/web/client/support/KotlinRestTemplateHttpServiceProxyTests.kt
@@ -0,0 +1,218 @@
+/*
+ * 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 okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+import org.springframework.lang.Nullable
+import org.springframework.util.LinkedMultiValueMap
+import org.springframework.util.MultiValueMap
+import org.springframework.web.bind.annotation.*
+import org.springframework.web.client.RestTemplate
+import org.springframework.web.multipart.MultipartFile
+import org.springframework.web.service.annotation.GetExchange
+import org.springframework.web.service.annotation.PostExchange
+import org.springframework.web.service.annotation.PutExchange
+import org.springframework.web.service.invoker.HttpServiceProxyFactory
+import org.springframework.web.testfixture.servlet.MockMultipartFile
+import org.springframework.web.util.DefaultUriBuilderFactory
+import java.net.URI
+import java.util.*
+
+/**
+ * Kotlin integration tests for {@link HttpServiceProxyFactory HTTP Service proxy} using
+ * {@link RestTemplate} and {@link MockWebServer}.
+ *
+ * @author Olga Maciaszek-Sharma
+ */
+class KotlinRestTemplateHttpServiceProxyTests {
+
+ private lateinit var server: MockWebServer
+
+ private lateinit var testService: TestService
+
+ @BeforeEach
+ fun setUp() {
+ server = MockWebServer()
+ prepareResponse()
+ testService = initTestService()
+ }
+
+ private fun initTestService(): TestService {
+ val restTemplate = RestTemplate()
+ restTemplate.uriTemplateHandler = DefaultUriBuilderFactory(server.url("/").toString())
+ return HttpServiceProxyFactory.builder()
+ .exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate))
+ .build()
+ .createClient(TestService::class.java)
+ }
+
+ @AfterEach
+ fun shutDown() {
+ server.shutdown()
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun getRequest() {
+ val response = testService.request
+
+ val request = server.takeRequest()
+ assertThat(response).isEqualTo("Hello Spring!")
+ assertThat(request.method).isEqualTo("GET")
+ assertThat(request.path).isEqualTo("/test")
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun getRequestWithPathVariable() {
+ val response = testService.getRequestWithPathVariable("456")
+
+ val request = server.takeRequest()
+ assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
+ assertThat(response.body).isEqualTo("Hello Spring!")
+ assertThat(request.method).isEqualTo("GET")
+ assertThat(request.path).isEqualTo("/test/456")
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun getRequestWithDynamicUri() {
+ val dynamicUri = server.url("/greeting/123").uri()
+
+ val response = testService.getRequestWithDynamicUri(dynamicUri, "456")
+
+ val request = server.takeRequest()
+ assertThat(response.orElse("empty")).isEqualTo("Hello Spring!")
+ assertThat(request.method).isEqualTo("GET")
+ assertThat(request.requestUrl.uri()).isEqualTo(dynamicUri)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun postWithRequestHeader() {
+ testService.postRequestWithHeader("testHeader", "testBody")
+
+ val request = server.takeRequest()
+ assertThat(request.method).isEqualTo("POST")
+ assertThat(request.path).isEqualTo("/test")
+ assertThat(request.headers["testHeaderName"]).isEqualTo("testHeader")
+ assertThat(request.body.readUtf8()).isEqualTo("testBody")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun formData() {
+ val map: MultiValueMap = LinkedMultiValueMap()
+ map.add("param1", "value 1")
+ map.add("param2", "value 2")
+
+ testService.postForm(map)
+
+ val request = server.takeRequest()
+ assertThat(request.headers["Content-Type"])
+ .isEqualTo("application/x-www-form-urlencoded;charset=UTF-8")
+ assertThat(request.body.readUtf8()).isEqualTo("param1=value+1¶m2=value+2")
+ }
+
+ // gh-30342
+ @Test
+ @Throws(InterruptedException::class)
+ fun multipart() {
+ val fileName = "testFileName"
+ val originalFileName = "originalTestFileName"
+ val file: MultipartFile = MockMultipartFile(fileName, originalFileName, MediaType.APPLICATION_JSON_VALUE,
+ "test".toByteArray())
+
+ testService.postMultipart(file, "test2")
+
+ val request = server.takeRequest()
+ assertThat(request.headers["Content-Type"]).startsWith("multipart/form-data;boundary=")
+ assertThat(request.body.readUtf8()).containsSubsequence(
+ "Content-Disposition: form-data; name=\"file\"; filename=\"originalTestFileName\"",
+ "Content-Type: application/json", "Content-Length: 4", "test",
+ "Content-Disposition: form-data; name=\"anotherPart\"", "Content-Type: text/plain;charset=UTF-8",
+ "Content-Length: 5", "test2")
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun putRequestWithCookies() {
+ testService.putRequestWithCookies("test1", "test2")
+
+ val request = server.takeRequest()
+ assertThat(request.method).isEqualTo("PUT")
+ assertThat(request.getHeader("Cookie"))
+ .isEqualTo("firstCookie=test1; secondCookie=test2")
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun putRequestWithSameNameCookies() {
+ testService.putRequestWithSameNameCookies("test1", "test2")
+
+ val request = server.takeRequest()
+ assertThat(request.method).isEqualTo("PUT")
+ assertThat(request.getHeader("Cookie"))
+ .isEqualTo("testCookie=test1; testCookie=test2")
+ }
+
+ private fun prepareResponse() {
+ val response = MockResponse()
+ response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")
+ server.enqueue(response)
+ }
+
+
+ private interface TestService {
+
+ @get:GetExchange("/test")
+ val request: String
+
+ @GetExchange("/test/{id}")
+ fun getRequestWithPathVariable(@PathVariable id: String): ResponseEntity
+
+ @GetExchange("/test/{id}")
+ fun getRequestWithDynamicUri(@Nullable uri: URI, @PathVariable id: String): Optional
+
+ @PostExchange("/test")
+ fun postRequestWithHeader(@RequestHeader("testHeaderName") testHeader: String,
+ @RequestBody requestBody: String)
+
+ @PostExchange(contentType = "application/x-www-form-urlencoded")
+ fun postForm(@RequestParam params: MultiValueMap)
+
+ @PostExchange
+ fun postMultipart(file: MultipartFile, @RequestPart anotherPart: String)
+
+ @PutExchange
+ fun putRequestWithCookies(@CookieValue firstCookie: String,
+ @CookieValue secondCookie: String)
+
+ @PutExchange
+ fun putRequestWithSameNameCookies(@CookieValue("testCookie") firstCookie: String,
+ @CookieValue("testCookie") secondCookie: String)
+ }
+
+}
\ No newline at end of file
diff --git a/spring-web/src/test/kotlin/org/springframework/web/service/invoker/HttpServiceMethodKotlinTests.kt b/spring-web/src/test/kotlin/org/springframework/web/service/invoker/HttpServiceMethodKotlinTests.kt
index 1fdb1a092e..c42c824d36 100644
--- a/spring-web/src/test/kotlin/org/springframework/web/service/invoker/HttpServiceMethodKotlinTests.kt
+++ b/spring-web/src/test/kotlin/org/springframework/web/service/invoker/HttpServiceMethodKotlinTests.kt
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatIllegalStateException
import org.junit.jupiter.api.Test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpStatus
@@ -30,64 +31,124 @@ import org.springframework.web.service.annotation.GetExchange
* Kotlin tests for [HttpServiceMethod].
*
* @author Sebastien Deleuze
+ * @author Olga Maciaszek-Sharma
*/
-class HttpServiceMethodKotlinTests {
+@Suppress("DEPRECATION")
+class KotlinHttpServiceMethodTests {
- private val client = TestHttpClientAdapter()
- private val proxyFactory = HttpServiceProxyFactory.builder(client).build()
+ private val webClientAdapter = TestHttpClientAdapter()
+ private val httpExchangeAdapter = TestHttpExchangeAdapter()
+ private val proxyFactory = HttpServiceProxyFactory.builder(webClientAdapter).build()
+ private val blockingProxyFactory = HttpServiceProxyFactory.builder()
+ .exchangeAdapter(httpExchangeAdapter).build()
@Test
fun coroutinesService(): Unit = runBlocking {
- val service = proxyFactory.createClient(CoroutinesService::class.java)
+ val service = proxyFactory.createClient(FunctionsService::class.java)
val stringBody = service.stringBody()
- assertThat(stringBody).isEqualTo("requestToBody")
- verifyClientInvocation("requestToBody", object : ParameterizedTypeReference() {})
+ assertThat(stringBody).isEqualTo("body")
+ verifyClientInvocation("body", object : ParameterizedTypeReference() {})
service.listBody()
- verifyClientInvocation("requestToBody", object : ParameterizedTypeReference>() {})
+ verifyClientInvocation("body", object : ParameterizedTypeReference>() {})
val flowBody = service.flowBody()
assertThat(flowBody.toList()).containsExactly("request", "To", "Body", "Flux")
- verifyClientInvocation("requestToBodyFlux", object : ParameterizedTypeReference() {})
+ verifyClientInvocation("bodyFlux", object : ParameterizedTypeReference() {})
val stringEntity = service.stringEntity()
- assertThat(stringEntity).isEqualTo(ResponseEntity.ok("requestToEntity"))
- verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference() {})
+ assertThat(stringEntity).isEqualTo(ResponseEntity.ok("entity"))
+ verifyClientInvocation("entity", object : ParameterizedTypeReference() {})
service.listEntity()
- verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference>() {})
+ verifyClientInvocation("entity", object : ParameterizedTypeReference>() {})
val flowEntity = service.flowEntity()
assertThat(flowEntity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(flowEntity.body!!.toList()).containsExactly("request", "To", "Entity", "Flux")
- verifyClientInvocation("requestToEntityFlux", object : ParameterizedTypeReference() {})
+ verifyClientInvocation("entityFlux", object : ParameterizedTypeReference() {})
}
- private fun verifyClientInvocation(methodName: String, expectedBodyType: ParameterizedTypeReference<*>) {
- assertThat(client.invokedMethodName).isEqualTo(methodName)
- assertThat(client.bodyType).isEqualTo(expectedBodyType)
+ @Test
+ fun blockingServiceWithExchangeResponseFunction() {
+ val service = blockingProxyFactory.createClient(BlockingFunctionsService::class.java)
+
+ val stringBody = service.stringBodyBlocking()
+ assertThat(stringBody).isEqualTo("body")
+ verifyTemplateInvocation("body", object : ParameterizedTypeReference() {})
+
+ val listBody = service.listBodyBlocking()
+ assertThat(listBody.size).isEqualTo(1)
+ verifyTemplateInvocation("body", object : ParameterizedTypeReference>() {})
+
+ val stringEntity = service.stringEntityBlocking()
+ assertThat(stringEntity).isEqualTo(ResponseEntity.ok("entity"))
+ verifyTemplateInvocation("entity", object : ParameterizedTypeReference() {})
+
+ service.listEntityBlocking()
+ verifyTemplateInvocation("entity", object : ParameterizedTypeReference>() {})
+ }
+
+ @Test
+ fun coroutineServiceWithExchangeResponseFunction() {
+ assertThatIllegalStateException().isThrownBy {
+ blockingProxyFactory.createClient(FunctionsService::class.java)
+ }
+
+ assertThatIllegalStateException().isThrownBy {
+ blockingProxyFactory.createClient(SuspendingFunctionsService::class.java)
+ }
+ }
+
+ private fun verifyTemplateInvocation(methodReference: String, expectedBodyType: ParameterizedTypeReference<*>) {
+ assertThat(httpExchangeAdapter.invokedMethodReference).isEqualTo(methodReference)
+ assertThat(httpExchangeAdapter.bodyType).isEqualTo(expectedBodyType)
+ }
+
+ private fun verifyClientInvocation(methodReference: String, expectedBodyType: ParameterizedTypeReference<*>) {
+ assertThat(webClientAdapter.invokedMethodReference).isEqualTo(methodReference)
+ assertThat(webClientAdapter.bodyType).isEqualTo(expectedBodyType)
}
- private interface CoroutinesService {
-
- @GetExchange
- suspend fun stringBody(): String
-
- @GetExchange
- suspend fun listBody(): MutableList
+ private interface FunctionsService : SuspendingFunctionsService {
@GetExchange
fun flowBody(): Flow
- @GetExchange
- suspend fun stringEntity(): ResponseEntity
-
- @GetExchange
- suspend fun listEntity(): ResponseEntity>
-
@GetExchange
fun flowEntity(): ResponseEntity>
}
+ private interface SuspendingFunctionsService : BlockingFunctionsService {
+
+ @GetExchange
+ suspend fun stringBody(): String
+
+ @GetExchange
+ suspend fun listBody(): MutableList
+
+ @GetExchange
+ suspend fun stringEntity(): ResponseEntity
+
+ @GetExchange
+ suspend fun listEntity(): ResponseEntity>
+ }
+
+ private interface BlockingFunctionsService {
+
+ @GetExchange
+ fun stringBodyBlocking(): String
+
+ @GetExchange
+ fun listBodyBlocking(): MutableList
+
+ @GetExchange
+ fun stringEntityBlocking(): ResponseEntity
+
+ @GetExchange
+ fun listEntityBlocking(): ResponseEntity>
+
+ }
+
}
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
index 2cb0f12ecc..2a9c99f4e6 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
@@ -25,10 +25,10 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.service.invoker.AbstractReactorHttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.invoker.ReactorHttpExchangeAdapter;
-import org.springframework.web.service.invoker.AbstractReactorHttpExchangeAdapter;
/**
* {@link ReactorHttpExchangeAdapter} that enables an {@link HttpServiceProxyFactory}
@@ -132,4 +132,8 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
return new WebClientAdapter(webClient);
}
+ @Override
+ public boolean supportsRequestAttributes() {
+ return true;
+ }
}
diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt
index ac9354cc27..9292ade029 100644
--- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt
+++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyKotlinTests.kt
@@ -41,7 +41,8 @@ import java.util.function.Consumer
* @author DongHyeon Kim
* @author Sebastien Deleuze
*/
-class WebClientHttpServiceProxyKotlinTests {
+@Suppress("DEPRECATION")
+class KotlinWebClientHttpServiceProxyTests {
private lateinit var server: MockWebServer