Add RestTemplate support for HTTP interface client
See gh-30117
This commit is contained in:
parent
bf82ed7186
commit
268f3c853e
|
|
@ -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"]
|
[source,java,indent=0,subs="verbatim,quotes"]
|
||||||
----
|
----
|
||||||
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
|
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);
|
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||||
----
|
----
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.web.client.support;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.HttpCookie;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.service.invoker.HttpExchangeAdapter;
|
||||||
|
import org.springframework.web.service.invoker.HttpRequestValues;
|
||||||
|
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link HttpExchangeAdapter} that enables an {@link HttpServiceProxyFactory} to use
|
||||||
|
* {@link RestTemplate} for request execution.
|
||||||
|
* <p>
|
||||||
|
* Use static factory methods in this class to create an {@link HttpServiceProxyFactory}
|
||||||
|
* configured with a given {@link RestTemplate}.
|
||||||
|
*
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @since 6.1
|
||||||
|
*/
|
||||||
|
public final class RestTemplateAdapter implements HttpExchangeAdapter {
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
// Private constructor; use static factory methods to instantiate
|
||||||
|
private RestTemplateAdapter(RestTemplate restTemplate) {
|
||||||
|
this.restTemplate = restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void exchange(HttpRequestValues requestValues) {
|
||||||
|
this.restTemplate.exchange(newRequest(requestValues), Void.class);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
|
||||||
|
return this.restTemplate.exchange(newRequest(requestValues), Void.class).getHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
|
return this.restTemplate.exchange(newRequest(requestValues), bodyType).getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
|
||||||
|
return this.restTemplate.exchange(newRequest(requestValues), Void.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues,
|
||||||
|
ParameterizedTypeReference<T> bodyType) {
|
||||||
|
return this.restTemplate.exchange(newRequest(requestValues), bodyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRequestAttributes() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestEntity<?> newRequest(HttpRequestValues requestValues) {
|
||||||
|
URI uri;
|
||||||
|
if (requestValues.getUri() != null) {
|
||||||
|
uri = requestValues.getUri();
|
||||||
|
}
|
||||||
|
else if (requestValues.getUriTemplate() != null) {
|
||||||
|
uri = this.restTemplate.getUriTemplateHandler().expand(requestValues.getUriTemplate(),
|
||||||
|
requestValues.getUriVariables());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalStateException("Neither full URL nor URI template");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpMethod httpMethod = requestValues.getHttpMethod();
|
||||||
|
Assert.notNull(httpMethod, "HttpMethod is required");
|
||||||
|
|
||||||
|
RequestEntity.BodyBuilder builder = RequestEntity.method(httpMethod, uri)
|
||||||
|
.headers(requestValues.getHeaders());
|
||||||
|
|
||||||
|
if (!requestValues.getCookies().isEmpty()) {
|
||||||
|
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
|
||||||
|
requestValues.getCookies()
|
||||||
|
.forEach((name, values) -> values.forEach(value ->
|
||||||
|
cookies.add(name, new HttpCookie(name, value))));
|
||||||
|
|
||||||
|
builder.header(HttpHeaders.COOKIE,
|
||||||
|
cookies.values()
|
||||||
|
.stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.map(HttpCookie::toString)
|
||||||
|
.collect(Collectors.joining("; ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestValues.getBodyValue() != null) {
|
||||||
|
return builder.body(requestValues.getBodyValue());
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link RestTemplateAdapter} for the given {@link RestTemplate} instance.
|
||||||
|
* @param restTemplate the {@link RestTemplate} to use
|
||||||
|
* @return the created adapter instance
|
||||||
|
*/
|
||||||
|
public static RestTemplateAdapter forTemplate(RestTemplate restTemplate) {
|
||||||
|
return new RestTemplateAdapter(restTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,10 @@ import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
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
|
* @author Rossen Stoyanchev
|
||||||
* @since 6.1
|
* @since 6.1
|
||||||
|
|
@ -51,16 +55,16 @@ public abstract class AbstractReactorHttpExchangeAdapter
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Configure the registry for adapting various reactive types.
|
||||||
* @param reactiveAdapterRegistry
|
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
|
||||||
|
* default settings.
|
||||||
*/
|
*/
|
||||||
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry reactiveAdapterRegistry) {
|
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry reactiveAdapterRegistry) {
|
||||||
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
|
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Return the configured reactive type registry of adapters.
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
|
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
|
||||||
|
|
@ -68,31 +72,31 @@ public abstract class AbstractReactorHttpExchangeAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Configure how long to block for the response of an HTTP service method with a
|
||||||
* @param blockTimeout
|
* synchronous (blocking) method signature.
|
||||||
|
* <p>
|
||||||
|
* By default, this is not set, in which case the behavior depends on connection and
|
||||||
|
* request timeout settings of the underlying HTTP client. We recommend configuring
|
||||||
|
* timeout values directly on the underlying HTTP client, which provides more
|
||||||
|
* control over such settings.
|
||||||
*/
|
*/
|
||||||
public void setBlockTimeout(@Nullable Duration blockTimeout) {
|
public void setBlockTimeout(@Nullable Duration blockTimeout) {
|
||||||
this.blockTimeout = blockTimeout;
|
this.blockTimeout = blockTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Duration getBlockTimeout() {
|
public Duration getBlockTimeout() {
|
||||||
return this.blockTimeout;
|
return this.blockTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exchange(HttpRequestValues requestValues) {
|
public Void exchange(HttpRequestValues requestValues) {
|
||||||
if (this.blockTimeout != null) {
|
if (this.blockTimeout != null) {
|
||||||
exchangeForMono(requestValues).block(this.blockTimeout);
|
return exchangeForMono(requestValues).block(this.blockTimeout);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
exchangeForMono(requestValues).block();
|
return exchangeForMono(requestValues).block();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import org.springframework.http.ResponseEntity;
|
||||||
* {@linkplain HttpServiceProxyFactory#createClient(Class) HTTP service proxy}.
|
* {@linkplain HttpServiceProxyFactory#createClient(Class) HTTP service proxy}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
* @since 6.0
|
* @since 6.0
|
||||||
* @deprecated in favor of {@link ReactorHttpExchangeAdapter}
|
* @deprecated in favor of {@link ReactorHttpExchangeAdapter}
|
||||||
*/
|
*/
|
||||||
|
|
@ -91,50 +92,58 @@ public interface HttpClientAdapter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapt this {@link HttpClientAdapter} to {@link ReactorHttpExchangeAdapter}.
|
* 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
|
* @since 6.1
|
||||||
*/
|
*/
|
||||||
default ReactorHttpExchangeAdapter asHttpExchangeAdapter() {
|
default ReactorHttpExchangeAdapter asHttpExchangeAdapter() {
|
||||||
|
|
||||||
|
HttpClientAdapter delegate = this;
|
||||||
|
|
||||||
return new AbstractReactorHttpExchangeAdapter() {
|
return new AbstractReactorHttpExchangeAdapter() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
|
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
|
||||||
return requestToVoid(requestValues);
|
return delegate.requestToVoid(requestValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues requestValues) {
|
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues requestValues) {
|
||||||
return requestToHeaders(requestValues);
|
return delegate.requestToHeaders(requestValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
return requestToBody(requestValues, bodyType);
|
return delegate.requestToBody(requestValues, bodyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
return requestToBodyFlux(requestValues, bodyType);
|
return delegate.requestToBodyFlux(requestValues, bodyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues requestValues) {
|
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues requestValues) {
|
||||||
return requestToBodilessEntity(requestValues);
|
return delegate.requestToBodilessEntity(requestValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Mono<ResponseEntity<T>> exchangeForEntityMono(
|
public <T> Mono<ResponseEntity<T>> exchangeForEntityMono(
|
||||||
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
|
|
||||||
return requestToEntity(requestValues, bodyType);
|
return delegate.requestToEntity(requestValues, bodyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(
|
public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(
|
||||||
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
|
|
||||||
return requestToEntityFlux(requestValues, bodyType);
|
return delegate.requestToEntityFlux(requestValues, bodyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRequestAttributes() {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public interface HttpExchangeAdapter {
|
||||||
* Perform the given request, and release the response content, if any.
|
* Perform the given request, and release the response content, if any.
|
||||||
* @param requestValues the request to perform
|
* @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
|
* 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.
|
* Perform the given request and decode the response content to the given type.
|
||||||
* @param requestValues the request to perform
|
* @param requestValues the request to perform
|
||||||
* @param bodyType the target type to decode to
|
* @param bodyType the target type to decode to
|
||||||
* @return the decoded response.
|
|
||||||
* @param <T> the type the response is decoded to
|
* @param <T> the type the response is decoded to
|
||||||
|
* @return the decoded response.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
<T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
|
<T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
|
||||||
|
|
@ -66,4 +66,9 @@ public interface HttpExchangeAdapter {
|
||||||
*/
|
*/
|
||||||
<T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
|
<T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag that indicates whether request attributes are supported by a specific client
|
||||||
|
* adapter.
|
||||||
|
*/
|
||||||
|
boolean supportsRequestAttributes();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import org.springframework.web.service.annotation.HttpExchange;
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
* @since 6.0
|
* @since 6.0
|
||||||
*/
|
*/
|
||||||
final class HttpServiceMethod {
|
final class HttpServiceMethod {
|
||||||
|
|
@ -282,17 +283,57 @@ final class HttpServiceMethod {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private record ExchangeResponseFunction(
|
private record ExchangeResponseFunction(
|
||||||
Function<HttpRequestValues, Object> responseFunction) implements ResponseFunction {
|
Function<HttpRequestValues, Object> responseFunction) implements ResponseFunction {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object execute(HttpRequestValues requestValues) {
|
public Object execute(HttpRequestValues requestValues) {
|
||||||
return null;
|
return this.responseFunction.apply(requestValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResponseFunction create(HttpExchangeAdapter client, Method method) {
|
public static ResponseFunction create(HttpExchangeAdapter client, Method method) {
|
||||||
return new ExchangeResponseFunction(httpRequestValues -> null);
|
if (KotlinDetector.isSuspendingFunction(method)) {
|
||||||
|
throw new IllegalStateException("Kotlin Coroutines are only supported with reactive implementations");
|
||||||
|
}
|
||||||
|
MethodParameter actualReturnParam = new MethodParameter(method, -1).nestedIfOptional();
|
||||||
|
boolean returnOptional = actualReturnParam.getParameterType().equals(Optional.class);
|
||||||
|
Class<?> actualReturnType = actualReturnParam.getNestedParameterType();
|
||||||
|
|
||||||
|
Function<HttpRequestValues, Object> responseFunction;
|
||||||
|
if (actualReturnType.equals(void.class) || actualReturnType.equals(Void.class)) {
|
||||||
|
responseFunction = client::exchange;
|
||||||
|
}
|
||||||
|
else if (actualReturnType.equals(HttpHeaders.class)) {
|
||||||
|
responseFunction = request -> processResponse(client.exchangeForHeaders(request),
|
||||||
|
returnOptional);
|
||||||
|
}
|
||||||
|
else if (actualReturnType.equals(ResponseEntity.class)) {
|
||||||
|
MethodParameter bodyParam = actualReturnParam.nested();
|
||||||
|
Class<?> bodyType = bodyParam.getNestedParameterType();
|
||||||
|
if (bodyType.equals(Void.class)) {
|
||||||
|
responseFunction = request -> processResponse(client
|
||||||
|
.exchangeForBodilessEntity(request), returnOptional);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ParameterizedTypeReference<?> bodyTypeReference = ParameterizedTypeReference
|
||||||
|
.forType(bodyParam.getNestedGenericParameterType());
|
||||||
|
responseFunction = request -> processResponse(client.exchangeForEntity(request,
|
||||||
|
bodyTypeReference), returnOptional);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ParameterizedTypeReference<?> bodyTypeReference = ParameterizedTypeReference
|
||||||
|
.forType(actualReturnParam.getNestedGenericParameterType());
|
||||||
|
responseFunction = request -> processResponse(client.exchangeForBody(request,
|
||||||
|
bodyTypeReference), returnOptional);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ExchangeResponseFunction(responseFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable Object processResponse(@Nullable Object response,
|
||||||
|
boolean returnOptional) {
|
||||||
|
return returnOptional ? Optional.ofNullable(response) : response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,7 @@ public final class HttpServiceProxyFactory {
|
||||||
this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver);
|
this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("DataFlowIssue")
|
||||||
private List<HttpServiceArgumentResolver> initArgumentResolvers() {
|
private List<HttpServiceArgumentResolver> initArgumentResolvers() {
|
||||||
|
|
||||||
// Custom
|
// Custom
|
||||||
|
|
@ -261,10 +262,12 @@ public final class HttpServiceProxyFactory {
|
||||||
resolvers.add(new RequestHeaderArgumentResolver(service));
|
resolvers.add(new RequestHeaderArgumentResolver(service));
|
||||||
resolvers.add(new RequestBodyArgumentResolver());
|
resolvers.add(new RequestBodyArgumentResolver());
|
||||||
resolvers.add(new PathVariableArgumentResolver(service));
|
resolvers.add(new PathVariableArgumentResolver(service));
|
||||||
|
if (this.exchangeAdapter.supportsRequestAttributes()) {
|
||||||
|
resolvers.add(new RequestAttributeArgumentResolver());
|
||||||
|
}
|
||||||
resolvers.add(new RequestParamArgumentResolver(service));
|
resolvers.add(new RequestParamArgumentResolver(service));
|
||||||
resolvers.add(new RequestPartArgumentResolver());
|
resolvers.add(new RequestPartArgumentResolver());
|
||||||
resolvers.add(new CookieValueArgumentResolver(service));
|
resolvers.add(new CookieValueArgumentResolver(service));
|
||||||
resolvers.add(new RequestAttributeArgumentResolver());
|
|
||||||
|
|
||||||
// Specific type
|
// Specific type
|
||||||
resolvers.add(new UrlArgumentResolver());
|
resolvers.add(new UrlArgumentResolver());
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.ReactiveAdapterRegistry;
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contract to abstract a Project Reactor, HTTP client to decouple it from the
|
* 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 {
|
public interface ReactorHttpExchangeAdapter extends HttpExchangeAdapter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Return the configured reactive type registry of adapters.
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
ReactiveAdapterRegistry getReactiveAdapterRegistry();
|
ReactiveAdapterRegistry getReactiveAdapterRegistry();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Return the configured time to block for the response of an HTTP service method with
|
||||||
|
* a synchronous (blocking) method signature.
|
||||||
*
|
*
|
||||||
* @return
|
* <p>
|
||||||
|
* By default, this is not set, in which case the behavior depends on connection and
|
||||||
|
* request timeout settings of the underlying HTTP client. We recommend configuring
|
||||||
|
* timeout values directly on the underlying HTTP client, which provides more *
|
||||||
|
* control over such settings.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
Duration getBlockTimeout();
|
Duration getBlockTimeout();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the given request, and release the response content, if any.
|
* Perform the given request, and release the response content, if any.
|
||||||
* @param requestValues the request to perform
|
* @param requestValues the request to perform
|
||||||
|
|
|
||||||
|
|
@ -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<String> 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<String> 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<String, String> 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<String> getRequestWithPathVariable(@PathVariable String id);
|
||||||
|
|
||||||
|
@GetExchange("/test/{id}")
|
||||||
|
Optional<String> 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<String, String> 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,18 +16,12 @@
|
||||||
|
|
||||||
package org.springframework.web.service.invoker;
|
package org.springframework.web.service.invoker;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
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 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.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
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;
|
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
|
* Base class for testing {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
|
||||||
* that stubs the client invocations.
|
* and a test {@link TestHttpExchangeAdapter} that stub the client invocations.
|
||||||
*
|
*
|
||||||
* <p>The tests do not create or invoke {@code HttpServiceMethod} directly but
|
* <p>
|
||||||
* rather use {@link HttpServiceProxyFactory} to create a service proxy in order to
|
* The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
|
||||||
* use a strongly typed interface without the need for class casts.
|
* {@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 Rossen Stoyanchev
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
class HttpServiceMethodTests {
|
abstract class HttpServiceMethodTests {
|
||||||
|
|
||||||
private static final ParameterizedTypeReference<String> BODY_TYPE = new ParameterizedTypeReference<>() {};
|
protected static final ParameterizedTypeReference<String> BODY_TYPE = new ParameterizedTypeReference<>() {
|
||||||
|
};
|
||||||
|
|
||||||
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
|
protected TestAdapter client;
|
||||||
|
|
||||||
private final HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.client).build();
|
protected HttpServiceProxyFactory proxyFactory;
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void reactorService() {
|
|
||||||
ReactorService service = this.proxyFactory.createClient(ReactorService.class);
|
|
||||||
|
|
||||||
Mono<Void> voidMono = service.execute();
|
|
||||||
StepVerifier.create(voidMono).verifyComplete();
|
|
||||||
verifyClientInvocation("requestToVoid", null);
|
|
||||||
|
|
||||||
Mono<HttpHeaders> headersMono = service.getHeaders();
|
|
||||||
StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
|
|
||||||
verifyClientInvocation("requestToHeaders", null);
|
|
||||||
|
|
||||||
Mono<String> body = service.getBody();
|
|
||||||
StepVerifier.create(body).expectNext("requestToBody").verifyComplete();
|
|
||||||
verifyClientInvocation("requestToBody", BODY_TYPE);
|
|
||||||
|
|
||||||
Flux<String> fluxBody = service.getFluxBody();
|
|
||||||
StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
|
|
||||||
verifyClientInvocation("requestToBodyFlux", BODY_TYPE);
|
|
||||||
|
|
||||||
Mono<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
|
||||||
StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
|
|
||||||
verifyClientInvocation("requestToBodilessEntity", null);
|
|
||||||
|
|
||||||
Mono<ResponseEntity<String>> entity = service.getEntity();
|
|
||||||
StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
|
|
||||||
verifyClientInvocation("requestToEntity", BODY_TYPE);
|
|
||||||
|
|
||||||
Mono<ResponseEntity<Flux<String>>> 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<HttpHeaders> headersSingle = service.getHeaders();
|
|
||||||
assertThat(headersSingle.blockingGet()).isNotNull();
|
|
||||||
|
|
||||||
Single<String> bodySingle = service.getBody();
|
|
||||||
assertThat(bodySingle.blockingGet()).isEqualTo("requestToBody");
|
|
||||||
|
|
||||||
Flowable<String> bodyFlow = service.getFlowableBody();
|
|
||||||
assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
|
|
||||||
|
|
||||||
Single<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
|
||||||
assertThat(voidEntity.blockingGet().getBody()).isNull();
|
|
||||||
|
|
||||||
Single<ResponseEntity<String>> entitySingle = service.getEntity();
|
|
||||||
assertThat(entitySingle.blockingGet().getBody()).isEqualTo("requestToEntity");
|
|
||||||
|
|
||||||
Single<ResponseEntity<Flowable<String>>> entityFlow = service.getFlowableEntity();
|
|
||||||
Flowable<String> body = (entityFlow.blockingGet()).getBody();
|
|
||||||
assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void blockingService() {
|
void blockingService() {
|
||||||
|
|
@ -131,16 +66,19 @@ class HttpServiceMethodTests {
|
||||||
assertThat(headers).isNotNull();
|
assertThat(headers).isNotNull();
|
||||||
|
|
||||||
String body = service.getBody();
|
String body = service.getBody();
|
||||||
assertThat(body).isEqualTo("requestToBody");
|
assertThat(body).isEqualTo(client.getInvokedMethodReference());
|
||||||
|
|
||||||
Optional<String> optional = service.getBodyOptional();
|
Optional<String> optional = service.getBodyOptional();
|
||||||
assertThat(optional).contains("requestToBody");
|
assertThat(optional).contains("body");
|
||||||
|
|
||||||
ResponseEntity<String> entity = service.getEntity();
|
ResponseEntity<String> entity = service.getEntity();
|
||||||
assertThat(entity.getBody()).isEqualTo("requestToEntity");
|
assertThat(entity.getBody()).isEqualTo("entity");
|
||||||
|
|
||||||
ResponseEntity<Void> voidEntity = service.getVoidEntity();
|
ResponseEntity<Void> voidEntity = service.getVoidEntity();
|
||||||
assertThat(voidEntity.getBody()).isNull();
|
assertThat(voidEntity.getBody()).isNull();
|
||||||
|
|
||||||
|
List<String> list = service.getList();
|
||||||
|
assertThat(list.get(0)).isEqualTo("body");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -166,9 +104,12 @@ class HttpServiceMethodTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void typeAndMethodAnnotatedService() {
|
void typeAndMethodAnnotatedService() {
|
||||||
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.client)
|
HttpExchangeAdapter actualClient = this.client instanceof HttpClientAdapter httpClient
|
||||||
.embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
|
? httpClient.asHttpExchangeAdapter() : (HttpExchangeAdapter) client;
|
||||||
.build();
|
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder()
|
||||||
|
.exchangeAdapter(actualClient)
|
||||||
|
.embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
|
||||||
|
.build();
|
||||||
|
|
||||||
MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class);
|
MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class);
|
||||||
|
|
||||||
|
|
@ -189,68 +130,11 @@ class HttpServiceMethodTests {
|
||||||
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
|
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
|
protected void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
|
||||||
assertThat(this.client.getInvokedMethodName()).isEqualTo(methodName);
|
assertThat(this.client.getInvokedMethodReference()).isEqualTo(methodName);
|
||||||
assertThat(this.client.getBodyType()).isEqualTo(expectedBodyType);
|
assertThat(this.client.getBodyType()).isEqualTo(expectedBodyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private interface ReactorService {
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Mono<Void> execute();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Mono<HttpHeaders> getHeaders();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Mono<String> getBody();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Flux<String> getFluxBody();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Mono<ResponseEntity<Void>> getVoidEntity();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Mono<ResponseEntity<String>> getEntity();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Mono<ResponseEntity<Flux<String>>> getFluxEntity();
|
|
||||||
|
|
||||||
default String getDefaultMethodValue() {
|
|
||||||
return "default value";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private interface RxJavaService {
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Completable execute();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Single<HttpHeaders> getHeaders();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Single<String> getBody();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Flowable<String> getFlowableBody();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Single<ResponseEntity<Void>> getVoidEntity();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Single<ResponseEntity<String>> getEntity();
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
Single<ResponseEntity<Flowable<String>>> getFlowableEntity();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private interface BlockingService {
|
private interface BlockingService {
|
||||||
|
|
||||||
|
|
@ -271,8 +155,11 @@ class HttpServiceMethodTests {
|
||||||
|
|
||||||
@GetExchange
|
@GetExchange
|
||||||
ResponseEntity<String> getEntity();
|
ResponseEntity<String> getEntity();
|
||||||
}
|
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
List<String> getList();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private interface MethodLevelAnnotatedService {
|
private interface MethodLevelAnnotatedService {
|
||||||
|
|
@ -285,10 +172,10 @@ class HttpServiceMethodTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@HttpExchange(url = "${baseUrl}", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE)
|
@HttpExchange(url = "${baseUrl}", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE)
|
||||||
private interface TypeAndMethodLevelAnnotatedService extends MethodLevelAnnotatedService {
|
private interface TypeAndMethodLevelAnnotatedService extends MethodLevelAnnotatedService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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<Void> voidMono = service.execute();
|
||||||
|
StepVerifier.create(voidMono).verifyComplete();
|
||||||
|
verifyClientInvocation("void", null);
|
||||||
|
|
||||||
|
Mono<HttpHeaders> headersMono = service.getHeaders();
|
||||||
|
StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
|
||||||
|
verifyClientInvocation("headers", null);
|
||||||
|
|
||||||
|
Mono<String> body = service.getBody();
|
||||||
|
StepVerifier.create(body).expectNext("body").verifyComplete();
|
||||||
|
verifyClientInvocation("body", BODY_TYPE);
|
||||||
|
|
||||||
|
Flux<String> fluxBody = service.getFluxBody();
|
||||||
|
StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
|
||||||
|
verifyClientInvocation("bodyFlux", BODY_TYPE);
|
||||||
|
|
||||||
|
Mono<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||||
|
StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
|
||||||
|
verifyClientInvocation("bodilessEntity", null);
|
||||||
|
|
||||||
|
Mono<ResponseEntity<String>> entity = service.getEntity();
|
||||||
|
StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
|
||||||
|
verifyClientInvocation("entity", BODY_TYPE);
|
||||||
|
|
||||||
|
Mono<ResponseEntity<Flux<String>>> 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<HttpHeaders> headersSingle = service.getHeaders();
|
||||||
|
assertThat(headersSingle.blockingGet()).isNotNull();
|
||||||
|
|
||||||
|
Single<String> bodySingle = service.getBody();
|
||||||
|
assertThat(bodySingle.blockingGet()).isEqualTo("body");
|
||||||
|
|
||||||
|
Flowable<String> bodyFlow = service.getFlowableBody();
|
||||||
|
assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
|
||||||
|
|
||||||
|
Single<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||||
|
assertThat(voidEntity.blockingGet().getBody()).isNull();
|
||||||
|
|
||||||
|
Single<ResponseEntity<String>> entitySingle = service.getEntity();
|
||||||
|
assertThat(entitySingle.blockingGet().getBody()).isEqualTo("entity");
|
||||||
|
|
||||||
|
Single<ResponseEntity<Flowable<String>>> entityFlow = service.getFlowableEntity();
|
||||||
|
Flowable<String> body = (entityFlow.blockingGet()).getBody();
|
||||||
|
assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ReactorService {
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Mono<Void> execute();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Mono<HttpHeaders> getHeaders();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Mono<String> getBody();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Flux<String> getFluxBody();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Mono<ResponseEntity<Void>> getVoidEntity();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Mono<ResponseEntity<String>> getEntity();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Mono<ResponseEntity<Flux<String>>> getFluxEntity();
|
||||||
|
|
||||||
|
default String getDefaultMethodValue() {
|
||||||
|
return "default value";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private interface RxJavaService {
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Completable execute();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Single<HttpHeaders> getHeaders();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Single<String> getBody();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Flowable<String> getFlowableBody();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Single<ResponseEntity<Void>> getVoidEntity();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Single<ResponseEntity<String>> getEntity();
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
Single<ResponseEntity<Flowable<String>>> getFlowableEntity();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.springframework.web.service.invoker;
|
package org.springframework.web.service.invoker;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
@ -33,10 +35,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* @author Olga Maciaszek-Sharma
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
class TestHttpClientAdapter implements HttpClientAdapter {
|
class TestHttpClientAdapter implements HttpClientAdapter, TestAdapter {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String invokedMethodName;
|
private String invokedForReturnMethodReference;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private HttpRequestValues requestValues;
|
private HttpRequestValues requestValues;
|
||||||
|
|
@ -44,17 +46,19 @@ class TestHttpClientAdapter implements HttpClientAdapter {
|
||||||
@Nullable
|
@Nullable
|
||||||
private ParameterizedTypeReference<?> bodyType;
|
private ParameterizedTypeReference<?> bodyType;
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getInvokedMethodName() {
|
public String getInvokedMethodReference() {
|
||||||
assertThat(this.invokedMethodName).isNotNull();
|
assertThat(this.invokedForReturnMethodReference).isNotNull();
|
||||||
return this.invokedMethodName;
|
return this.invokedForReturnMethodReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public HttpRequestValues getRequestValues() {
|
public HttpRequestValues getRequestValues() {
|
||||||
assertThat(this.requestValues).isNotNull();
|
assertThat(this.requestValues).isNotNull();
|
||||||
return this.requestValues;
|
return this.requestValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public ParameterizedTypeReference<?> getBodyType() {
|
public ParameterizedTypeReference<?> getBodyType() {
|
||||||
return this.bodyType;
|
return this.bodyType;
|
||||||
|
|
@ -65,31 +69,33 @@ class TestHttpClientAdapter implements HttpClientAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> requestToVoid(HttpRequestValues requestValues) {
|
public Mono<Void> requestToVoid(HttpRequestValues requestValues) {
|
||||||
saveInput("requestToVoid", requestValues, null);
|
saveInput("void", requestValues, null);
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<HttpHeaders> requestToHeaders(HttpRequestValues requestValues) {
|
public Mono<HttpHeaders> requestToHeaders(HttpRequestValues requestValues) {
|
||||||
saveInput("requestToHeaders", requestValues, null);
|
saveInput("headers", requestValues, null);
|
||||||
return Mono.just(new HttpHeaders());
|
return Mono.just(new HttpHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Mono<T> requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
public <T> Mono<T> requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
saveInput("requestToBody", requestValues, bodyType);
|
saveInput("body", requestValues, bodyType);
|
||||||
return (Mono<T>) Mono.just(getInvokedMethodName());
|
return bodyType.getType().getTypeName().contains("List") ?
|
||||||
|
(Mono<T>) Mono.just(Collections.singletonList(getInvokedMethodReference()))
|
||||||
|
: (Mono<T>) Mono.just(getInvokedMethodReference());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Flux<T> requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
public <T> Flux<T> requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
saveInput("requestToBodyFlux", requestValues, bodyType);
|
saveInput("bodyFlux", requestValues, bodyType);
|
||||||
return (Flux<T>) Flux.just("request", "To", "Body", "Flux");
|
return (Flux<T>) Flux.just("request", "To", "Body", "Flux");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestValues requestValues) {
|
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestValues requestValues) {
|
||||||
saveInput("requestToBodilessEntity", requestValues, null);
|
saveInput("bodilessEntity", requestValues, null);
|
||||||
return Mono.just(ResponseEntity.ok().build());
|
return Mono.just(ResponseEntity.ok().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,22 +103,22 @@ class TestHttpClientAdapter implements HttpClientAdapter {
|
||||||
public <T> Mono<ResponseEntity<T>> requestToEntity(
|
public <T> Mono<ResponseEntity<T>> requestToEntity(
|
||||||
HttpRequestValues requestValues, ParameterizedTypeReference<T> type) {
|
HttpRequestValues requestValues, ParameterizedTypeReference<T> type) {
|
||||||
|
|
||||||
saveInput("requestToEntity", requestValues, type);
|
saveInput("entity", requestValues, type);
|
||||||
return Mono.just((ResponseEntity<T>) ResponseEntity.ok("requestToEntity"));
|
return Mono.just((ResponseEntity<T>) ResponseEntity.ok("entity"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(
|
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(
|
||||||
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
|
|
||||||
saveInput("requestToEntityFlux", requestValues, bodyType);
|
saveInput("entityFlux", requestValues, bodyType);
|
||||||
return Mono.just(ResponseEntity.ok((Flux<T>) Flux.just("request", "To", "Entity", "Flux")));
|
return Mono.just(ResponseEntity.ok((Flux<T>) Flux.just("request", "To", "Entity", "Flux")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> void saveInput(
|
private <T> void saveInput(
|
||||||
String methodName, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference<T> bodyType) {
|
String reference, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference<T> bodyType) {
|
||||||
|
|
||||||
this.invokedMethodName = methodName;
|
this.invokedForReturnMethodReference = reference;
|
||||||
this.requestValues = requestValues;
|
this.requestValues = requestValues;
|
||||||
this.bodyType = bodyType;
|
this.bodyType = bodyType;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||||
|
saveInput("body", requestValues, bodyType);
|
||||||
|
return bodyType.getType().getTypeName().contains("List")
|
||||||
|
? (T) Collections.singletonList(getInvokedMethodReference()) : (T) getInvokedMethodReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
|
||||||
|
saveInput("bodilessEntity", requestValues, null);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues,
|
||||||
|
ParameterizedTypeReference<T> bodyType) {
|
||||||
|
saveInput("entity", requestValues, bodyType);
|
||||||
|
return (ResponseEntity<T>) ResponseEntity.ok(this.getInvokedMethodReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRequestAttributes() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> void saveInput(String methodName, HttpRequestValues requestValues,
|
||||||
|
@Nullable ParameterizedTypeReference<T> bodyType) {
|
||||||
|
|
||||||
|
this.invokedMethodName = methodName;
|
||||||
|
this.requestValues = requestValues;
|
||||||
|
this.bodyType = bodyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<String, String> = 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<String>
|
||||||
|
|
||||||
|
@GetExchange("/test/{id}")
|
||||||
|
fun getRequestWithDynamicUri(@Nullable uri: URI, @PathVariable id: String): Optional<String>
|
||||||
|
|
||||||
|
@PostExchange("/test")
|
||||||
|
fun postRequestWithHeader(@RequestHeader("testHeaderName") testHeader: String,
|
||||||
|
@RequestBody requestBody: String)
|
||||||
|
|
||||||
|
@PostExchange(contentType = "application/x-www-form-urlencoded")
|
||||||
|
fun postForm(@RequestParam params: MultiValueMap<String, String>)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatIllegalStateException
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.core.ParameterizedTypeReference
|
import org.springframework.core.ParameterizedTypeReference
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
|
@ -30,64 +31,124 @@ import org.springframework.web.service.annotation.GetExchange
|
||||||
* Kotlin tests for [HttpServiceMethod].
|
* Kotlin tests for [HttpServiceMethod].
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
class HttpServiceMethodKotlinTests {
|
@Suppress("DEPRECATION")
|
||||||
|
class KotlinHttpServiceMethodTests {
|
||||||
|
|
||||||
private val client = TestHttpClientAdapter()
|
private val webClientAdapter = TestHttpClientAdapter()
|
||||||
private val proxyFactory = HttpServiceProxyFactory.builder(client).build()
|
private val httpExchangeAdapter = TestHttpExchangeAdapter()
|
||||||
|
private val proxyFactory = HttpServiceProxyFactory.builder(webClientAdapter).build()
|
||||||
|
private val blockingProxyFactory = HttpServiceProxyFactory.builder()
|
||||||
|
.exchangeAdapter(httpExchangeAdapter).build()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun coroutinesService(): Unit = runBlocking {
|
fun coroutinesService(): Unit = runBlocking {
|
||||||
val service = proxyFactory.createClient(CoroutinesService::class.java)
|
val service = proxyFactory.createClient(FunctionsService::class.java)
|
||||||
|
|
||||||
val stringBody = service.stringBody()
|
val stringBody = service.stringBody()
|
||||||
assertThat(stringBody).isEqualTo("requestToBody")
|
assertThat(stringBody).isEqualTo("body")
|
||||||
verifyClientInvocation("requestToBody", object : ParameterizedTypeReference<String>() {})
|
verifyClientInvocation("body", object : ParameterizedTypeReference<String>() {})
|
||||||
|
|
||||||
service.listBody()
|
service.listBody()
|
||||||
verifyClientInvocation("requestToBody", object : ParameterizedTypeReference<MutableList<String>>() {})
|
verifyClientInvocation("body", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||||
|
|
||||||
val flowBody = service.flowBody()
|
val flowBody = service.flowBody()
|
||||||
assertThat(flowBody.toList()).containsExactly("request", "To", "Body", "Flux")
|
assertThat(flowBody.toList()).containsExactly("request", "To", "Body", "Flux")
|
||||||
verifyClientInvocation("requestToBodyFlux", object : ParameterizedTypeReference<String>() {})
|
verifyClientInvocation("bodyFlux", object : ParameterizedTypeReference<String>() {})
|
||||||
|
|
||||||
val stringEntity = service.stringEntity()
|
val stringEntity = service.stringEntity()
|
||||||
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("requestToEntity"))
|
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("entity"))
|
||||||
verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference<String>() {})
|
verifyClientInvocation("entity", object : ParameterizedTypeReference<String>() {})
|
||||||
|
|
||||||
service.listEntity()
|
service.listEntity()
|
||||||
verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference<MutableList<String>>() {})
|
verifyClientInvocation("entity", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||||
|
|
||||||
val flowEntity = service.flowEntity()
|
val flowEntity = service.flowEntity()
|
||||||
assertThat(flowEntity.statusCode).isEqualTo(HttpStatus.OK)
|
assertThat(flowEntity.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
assertThat(flowEntity.body!!.toList()).containsExactly("request", "To", "Entity", "Flux")
|
assertThat(flowEntity.body!!.toList()).containsExactly("request", "To", "Entity", "Flux")
|
||||||
verifyClientInvocation("requestToEntityFlux", object : ParameterizedTypeReference<String>() {})
|
verifyClientInvocation("entityFlux", object : ParameterizedTypeReference<String>() {})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyClientInvocation(methodName: String, expectedBodyType: ParameterizedTypeReference<*>) {
|
@Test
|
||||||
assertThat(client.invokedMethodName).isEqualTo(methodName)
|
fun blockingServiceWithExchangeResponseFunction() {
|
||||||
assertThat(client.bodyType).isEqualTo(expectedBodyType)
|
val service = blockingProxyFactory.createClient(BlockingFunctionsService::class.java)
|
||||||
|
|
||||||
|
val stringBody = service.stringBodyBlocking()
|
||||||
|
assertThat(stringBody).isEqualTo("body")
|
||||||
|
verifyTemplateInvocation("body", object : ParameterizedTypeReference<String>() {})
|
||||||
|
|
||||||
|
val listBody = service.listBodyBlocking()
|
||||||
|
assertThat(listBody.size).isEqualTo(1)
|
||||||
|
verifyTemplateInvocation("body", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||||
|
|
||||||
|
val stringEntity = service.stringEntityBlocking()
|
||||||
|
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("entity"))
|
||||||
|
verifyTemplateInvocation("entity", object : ParameterizedTypeReference<String>() {})
|
||||||
|
|
||||||
|
service.listEntityBlocking()
|
||||||
|
verifyTemplateInvocation("entity", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
private interface FunctionsService : SuspendingFunctionsService {
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
suspend fun stringBody(): String
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
suspend fun listBody(): MutableList<String>
|
|
||||||
|
|
||||||
@GetExchange
|
@GetExchange
|
||||||
fun flowBody(): Flow<String>
|
fun flowBody(): Flow<String>
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
suspend fun stringEntity(): ResponseEntity<String>
|
|
||||||
|
|
||||||
@GetExchange
|
|
||||||
suspend fun listEntity(): ResponseEntity<MutableList<String>>
|
|
||||||
|
|
||||||
@GetExchange
|
@GetExchange
|
||||||
fun flowEntity(): ResponseEntity<Flow<String>>
|
fun flowEntity(): ResponseEntity<Flow<String>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interface SuspendingFunctionsService : BlockingFunctionsService {
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
suspend fun stringBody(): String
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
suspend fun listBody(): MutableList<String>
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
suspend fun stringEntity(): ResponseEntity<String>
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
suspend fun listEntity(): ResponseEntity<MutableList<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface BlockingFunctionsService {
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
fun stringBodyBlocking(): String
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
fun listBodyBlocking(): MutableList<String>
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
fun stringEntityBlocking(): ResponseEntity<String>
|
||||||
|
|
||||||
|
@GetExchange
|
||||||
|
fun listEntityBlocking(): ResponseEntity<MutableList<String>>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
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.HttpRequestValues;
|
||||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||||
import org.springframework.web.service.invoker.ReactorHttpExchangeAdapter;
|
import org.springframework.web.service.invoker.ReactorHttpExchangeAdapter;
|
||||||
import org.springframework.web.service.invoker.AbstractReactorHttpExchangeAdapter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ReactorHttpExchangeAdapter} that enables an {@link HttpServiceProxyFactory}
|
* {@link ReactorHttpExchangeAdapter} that enables an {@link HttpServiceProxyFactory}
|
||||||
|
|
@ -132,4 +132,8 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
|
||||||
return new WebClientAdapter(webClient);
|
return new WebClientAdapter(webClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRequestAttributes() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ import java.util.function.Consumer
|
||||||
* @author DongHyeon Kim
|
* @author DongHyeon Kim
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
*/
|
*/
|
||||||
class WebClientHttpServiceProxyKotlinTests {
|
@Suppress("DEPRECATION")
|
||||||
|
class KotlinWebClientHttpServiceProxyTests {
|
||||||
|
|
||||||
private lateinit var server: MockWebServer
|
private lateinit var server: MockWebServer
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue