Add ReactiveHttpRequestValues

Separate collection and handling of reactive request values into a
subclass of HttpRequestValues.

Closes gh-30117
This commit is contained in:
rstoyanchev 2023-07-11 07:42:04 +01:00
parent 3209cf5c7a
commit a3e37597aa
9 changed files with 436 additions and 104 deletions

View File

@ -27,10 +27,10 @@ import org.reactivestreams.Publisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@ -48,7 +48,7 @@ import org.springframework.web.util.UriUtils;
* @author Rossen Stoyanchev
* @since 6.0
*/
public final class HttpRequestValues {
public class HttpRequestValues {
private static final MultiValueMap<String, String> EMPTY_COOKIES_MAP =
CollectionUtils.toMultiValueMap(Collections.emptyMap());
@ -74,18 +74,11 @@ public final class HttpRequestValues {
@Nullable
private final Object bodyValue;
@Nullable
private final Publisher<?> body;
@Nullable
private final ParameterizedTypeReference<?> bodyElementType;
private HttpRequestValues(@Nullable HttpMethod httpMethod,
protected HttpRequestValues(@Nullable HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
@Nullable Object bodyValue,
@Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> bodyElementType) {
@Nullable Object bodyValue) {
Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template");
@ -97,8 +90,6 @@ public final class HttpRequestValues {
this.cookies = cookies;
this.attributes = attributes;
this.bodyValue = bodyValue;
this.body = body;
this.bodyElementType = bodyElementType;
}
@ -161,8 +152,6 @@ public final class HttpRequestValues {
/**
* Return the request body as a value to be serialized, if set.
* <p>This is mutually exclusive with {@link #getBody()}.
* Only one of the two or neither is set.
*/
@Nullable
public Object getBodyValue() {
@ -173,18 +162,24 @@ public final class HttpRequestValues {
* Return the request body as a Publisher.
* <p>This is mutually exclusive with {@link #getBodyValue()}.
* Only one of the two or neither is set.
* @deprecated in favor of {@link ReactiveHttpRequestValues#getBodyPublisher()};
* to be removed in 6.2
*/
@Deprecated(since = "6.1", forRemoval = true)
@Nullable
public Publisher<?> getBody() {
return this.body;
throw new UnsupportedOperationException();
}
/**
* Return the element type for a {@linkplain #getBody() Publisher body}.
* Return the element type for a Publisher body.
* @deprecated in favor of {@link ReactiveHttpRequestValues#getBodyPublisherElementType()};
* to be removed in 6.2
*/
@Deprecated(since = "6.1", forRemoval = true)
@Nullable
public ParameterizedTypeReference<?> getBodyElementType() {
return this.bodyElementType;
throw new UnsupportedOperationException();
}
@ -196,7 +191,7 @@ public final class HttpRequestValues {
/**
* Builder for {@link HttpRequestValues}.
*/
public final static class Builder {
public static class Builder {
@Nullable
private HttpMethod httpMethod;
@ -220,7 +215,7 @@ public final class HttpRequestValues {
private MultiValueMap<String, String> requestParams;
@Nullable
private MultipartBodyBuilder multipartBuilder;
private MultiValueMap<String, Object> parts;
@Nullable
private Map<String, Object> attributes;
@ -228,12 +223,6 @@ public final class HttpRequestValues {
@Nullable
private Object bodyValue;
@Nullable
private Publisher<?> body;
@Nullable
private ParameterizedTypeReference<?> bodyElementType;
/**
* Set the HTTP method for the request.
*/
@ -327,23 +316,30 @@ public final class HttpRequestValues {
}
/**
* Add a part to a multipart request. The part value may be as described
* in {@link MultipartBodyBuilder#part(String, Object)}.
* Add a part for a multipart request. The part may be:
* <ul>
* <li>String -- form field
* <li>{@link org.springframework.core.io.Resource Resource} -- file part
* <li>Object -- content to be encoded (e.g. to JSON)
* <li>{@link HttpEntity} -- part content and headers although generally it's
* easier to add headers through the returned builder
* </ul>
*/
public Builder addRequestPart(String name, Object part) {
this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder());
this.multipartBuilder.part(name, part);
this.parts = (this.parts != null ? this.parts : new LinkedMultiValueMap<>());
this.parts.add(name, part);
return this;
}
/**
* Variant of {@link #addRequestPart(String, Object)} that allows the
* part value to be produced by a {@link Publisher}.
* @deprecated in favor of {@link ReactiveHttpRequestValues.Builder#addRequestPartPublisher};
* to be removed in 6.2
*/
@Deprecated(since = "6.1", forRemoval = true)
public <T, P extends Publisher<T>> Builder addRequestPart(String name, P publisher, ResolvableType type) {
this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder());
this.multipartBuilder.asyncPart(name, publisher, ParameterizedTypeReference.forType(type.getType()));
return this;
throw new UnsupportedOperationException();
}
/**
@ -358,25 +354,22 @@ public final class HttpRequestValues {
}
/**
* Set the request body as a concrete value to be serialized.
* <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setBody(Publisher, ParameterizedTypeReference) body Publisher}.
* Set the request body as an Object to be serialized.
*/
public void setBodyValue(Object bodyValue) {
this.bodyValue = bodyValue;
this.body = null;
this.bodyElementType = null;
}
/**
* Set the request body as a concrete value to be serialized.
* Set the request body as a Reactive Streams Publisher.
* <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setBodyValue(Object) body value}.
* @deprecated in favor of {@link ReactiveHttpRequestValues.Builder#setBodyPublisher};
* to be removed in 6.2
*/
@Deprecated(since = "6.1", forRemoval = true)
public <T, P extends Publisher<T>> void setBody(P body, ParameterizedTypeReference<T> elementTye) {
this.body = body;
this.bodyElementType = elementTye;
this.bodyValue = null;
throw new UnsupportedOperationException();
}
/**
@ -389,15 +382,15 @@ public final class HttpRequestValues {
Map<String, String> uriVars = (this.uriVars != null ? new HashMap<>(this.uriVars) : Collections.emptyMap());
Object bodyValue = this.bodyValue;
if (this.multipartBuilder != null) {
Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request parts, not both");
bodyValue = this.multipartBuilder.build();
if (hasParts()) {
Assert.isTrue(!hasBody(), "Expected body or request parts, not both");
bodyValue = buildMultipartBody();
}
if (!CollectionUtils.isEmpty(this.requestParams)) {
if (hasContentType(MediaType.APPLICATION_FORM_URLENCODED)) {
Assert.isTrue(this.multipartBuilder == null, "Cannot add parts to form data request");
Assert.isTrue(bodyValue == null && this.body == null, "Cannot set body of form data request");
if (hasFormDataContentType()) {
Assert.isTrue(!hasParts(), "Request parts not expected for a form data request");
Assert.isTrue(!hasBody(), "Body not expected for a form data request");
bodyValue = new LinkedMultiValueMap<>(this.requestParams);
}
else if (uri != null) {
@ -426,13 +419,26 @@ public final class HttpRequestValues {
Map<String, Object> attributes = (this.attributes != null ?
new HashMap<>(this.attributes) : Collections.emptyMap());
return new HttpRequestValues(
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes,
bodyValue, this.body, this.bodyElementType);
return createRequestValues(
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, bodyValue);
}
private boolean hasContentType(MediaType mediaType) {
return (this.headers != null && mediaType.equals(this.headers.getContentType()));
protected boolean hasParts() {
return (this.parts != null);
}
protected boolean hasBody() {
return (this.bodyValue != null);
}
protected Object buildMultipartBody() {
Assert.notNull(this.parts, "`parts` is null, was hasParts() not called?");
return this.parts;
}
private boolean hasFormDataContentType() {
return (this.headers != null &&
MediaType.APPLICATION_FORM_URLENCODED.equals(this.headers.getContentType()));
}
private String appendQueryParams(
@ -453,6 +459,15 @@ public final class HttpRequestValues {
return uriComponentsBuilder.build().toUriString();
}
protected HttpRequestValues createRequestValues(
@Nullable HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVars,
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
@Nullable Object bodyValue) {
return new HttpRequestValues(
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, bodyValue);
}
}
}

View File

@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -81,13 +82,16 @@ final class HttpServiceMethod {
this.parameters = initMethodParameters(method);
this.argumentResolvers = argumentResolvers;
this.requestValuesInitializer =
HttpRequestValuesInitializer.create(method, containingClass, embeddedValueResolver);
boolean isReactorAdapter = (REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter);
this.responseFunction =
(REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter reactorAdapter ?
ReactorExchangeResponseFunction.create(reactorAdapter, method) :
ExchangeResponseFunction.create(adapter, method));
this.requestValuesInitializer =
HttpRequestValuesInitializer.create(
method, containingClass, embeddedValueResolver,
(isReactorAdapter ? ReactiveHttpRequestValues::builder : HttpRequestValues::builder));
this.responseFunction = (isReactorAdapter ?
ReactorExchangeResponseFunction.create((ReactorHttpExchangeAdapter) adapter, method) :
ExchangeResponseFunction.create(adapter, method));
}
private static MethodParameter[] initMethodParameters(Method method) {
@ -147,20 +151,11 @@ final class HttpServiceMethod {
*/
private record HttpRequestValuesInitializer(
@Nullable HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
private HttpRequestValuesInitializer(
HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
this.url = url;
this.httpMethod = httpMethod;
this.contentType = contentType;
this.acceptMediaTypes = acceptMediaTypes;
}
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes,
Supplier<HttpRequestValues.Builder> requestValuesSupplier) {
public HttpRequestValues.Builder initializeRequestValuesBuilder() {
HttpRequestValues.Builder requestValues = HttpRequestValues.builder();
HttpRequestValues.Builder requestValues = this.requestValuesSupplier.get();
if (this.httpMethod != null) {
requestValues.setHttpMethod(this.httpMethod);
}
@ -181,7 +176,8 @@ final class HttpServiceMethod {
* Introspect the method and create the request factory for it.
*/
public static HttpRequestValuesInitializer create(
Method method, Class<?> containingClass, @Nullable StringValueResolver embeddedValueResolver) {
Method method, Class<?> containingClass, @Nullable StringValueResolver embeddedValueResolver,
Supplier<HttpRequestValues.Builder> requestValuesSupplier) {
HttpExchange annot1 = AnnotatedElementUtils.findMergedAnnotation(containingClass, HttpExchange.class);
HttpExchange annot2 = AnnotatedElementUtils.findMergedAnnotation(method, HttpExchange.class);
@ -193,7 +189,8 @@ final class HttpServiceMethod {
MediaType contentType = initContentType(annot1, annot2);
List<MediaType> acceptableMediaTypes = initAccept(annot1, annot2);
return new HttpRequestValuesInitializer(httpMethod, url, contentType, acceptableMediaTypes);
return new HttpRequestValuesInitializer(
httpMethod, url, contentType, acceptableMediaTypes, requestValuesSupplier);
}
@Nullable

View File

@ -0,0 +1,275 @@
/*
* 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.net.URI;
import java.util.List;
import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/**
* {@link HttpRequestValues} extension for use with {@link ReactorHttpExchangeAdapter}.
*
* @author Rossen Stoyanchev
* @since 6.1
*/
public final class ReactiveHttpRequestValues extends HttpRequestValues {
@Nullable
private final Publisher<?> body;
@Nullable
private final ParameterizedTypeReference<?> bodyElementType;
private ReactiveHttpRequestValues(
@Nullable HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
@Nullable Object bodyValue, @Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> elementType) {
super(httpMethod, uri, uriTemplate, uriVariables, headers, cookies, attributes, bodyValue);
this.body = body;
this.bodyElementType = elementType;
}
/**
* Return a {@link Publisher} that will produce for the request body.
* <p>This is mutually exclusive with {@link #getBodyValue()}.
* Only one of the two or neither is set.
*/
@Nullable
public Publisher<?> getBodyPublisher() {
return this.body;
}
/**
* Return the element type for a {@linkplain #getBodyPublisher() Publisher body}.
*/
@Nullable
public ParameterizedTypeReference<?> getBodyPublisherElementType() {
return this.bodyElementType;
}
/**
* Return the request body as a Publisher.
* <p>This is mutually exclusive with {@link #getBodyValue()}.
* Only one of the two or neither is set.
*/
@SuppressWarnings("removal")
@Nullable
public Publisher<?> getBody() {
return getBodyPublisher();
}
/**
* Return the element type for a {@linkplain #getBodyPublisher() Publisher body}.
*/
@SuppressWarnings("removal")
@Nullable
public ParameterizedTypeReference<?> getBodyElementType() {
return getBodyPublisherElementType();
}
public static Builder builder() {
return new Builder();
}
/**
* Builder for {@link ReactiveHttpRequestValues}.
*/
public final static class Builder extends HttpRequestValues.Builder {
@Nullable
private MultipartBodyBuilder multipartBuilder;
@Nullable
private Publisher<?> body;
@Nullable
private ParameterizedTypeReference<?> bodyElementType;
@Override
public Builder setHttpMethod(HttpMethod httpMethod) {
super.setHttpMethod(httpMethod);
return this;
}
@Override
public Builder setUri(URI uri) {
super.setUri(uri);
return this;
}
@Override
public Builder setUriTemplate(String uriTemplate) {
super.setUriTemplate(uriTemplate);
return this;
}
@Override
public Builder setUriVariable(String name, String value) {
super.setUriVariable(name, value);
return this;
}
@Override
public Builder setAccept(List<MediaType> acceptableMediaTypes) {
super.setAccept(acceptableMediaTypes);
return this;
}
@Override
public Builder setContentType(MediaType contentType) {
super.setContentType(contentType);
return this;
}
@Override
public Builder addHeader(String headerName, String... headerValues) {
super.addHeader(headerName, headerValues);
return this;
}
@Override
public Builder addCookie(String name, String... values) {
super.addCookie(name, values);
return this;
}
@Override
public Builder addRequestParameter(String name, String... values) {
super.addRequestParameter(name, values);
return this;
}
@Override
public Builder addAttribute(String name, Object value) {
super.addAttribute(name, value);
return this;
}
/**
* Add a part to a multipart request. The part value may be as described
* in {@link MultipartBodyBuilder#part(String, Object)}.
*/
@Override
public Builder addRequestPart(String name, Object part) {
this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder());
this.multipartBuilder.part(name, part);
return this;
}
/**
* Variant of {@link #addRequestPart(String, Object)} that allows the
* part value to be produced by a {@link Publisher}.
*/
public <T, P extends Publisher<T>> Builder addRequestPartPublisher(
String name, P publisher, ParameterizedTypeReference<T> elementTye) {
this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder());
this.multipartBuilder.asyncPart(name, publisher, elementTye);
return this;
}
@SuppressWarnings("removal")
@Override
public <T, P extends Publisher<T>> Builder addRequestPart(String name, P publisher, ResolvableType type) {
return addRequestPartPublisher(name, publisher, ParameterizedTypeReference.forType(type.getType()));
}
/**
* {@inheritDoc}
* <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setBodyPublisher(Publisher, ParameterizedTypeReference)}.
*/
@Override
public void setBodyValue(Object bodyValue) {
super.setBodyValue(bodyValue);
this.body = null;
this.bodyElementType = null;
}
/**
* Set the request body as a Reactive Streams Publisher.
* <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setBodyValue(Object) body value}.
*/
@SuppressWarnings("DataFlowIssue")
public <T, P extends Publisher<T>> void setBodyPublisher(P body, ParameterizedTypeReference<T> elementTye) {
this.body = body;
this.bodyElementType = elementTye;
super.setBodyValue(null);
}
@SuppressWarnings("removal")
@Override
public <T, P extends Publisher<T>> void setBody(P body, ParameterizedTypeReference<T> elementTye) {
setBodyPublisher(body, elementTye);
}
@Override
public ReactiveHttpRequestValues build() {
return (ReactiveHttpRequestValues) super.build();
}
@Override
protected boolean hasParts() {
return (this.multipartBuilder != null);
}
@Override
protected boolean hasBody() {
return (super.hasBody() || this.body != null);
}
@Override
protected Object buildMultipartBody() {
Assert.notNull(this.multipartBuilder, "`multipartBuilder` is null, was hasParts() not called?");
return this.multipartBuilder.build();
}
@Override
protected ReactiveHttpRequestValues createRequestValues(
@Nullable HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVars,
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
@Nullable Object bodyValue) {
return new ReactiveHttpRequestValues(
httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes,
bodyValue, this.body, this.bodyElementType);
}
}
}

View File

@ -78,9 +78,14 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
Assert.isTrue(!adapter.isNoValue(), message);
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
requestValues.setBody(
adapter.toPublisher(argument),
ParameterizedTypeReference.forType(nestedParameter.getNestedGenericParameterType()));
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) {
reactiveRequestValues.setBodyPublisher(
adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter));
}
else {
throw new IllegalStateException(
"RequestBody with a reactive type is only supported with reactive client");
}
return true;
}
@ -93,4 +98,8 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
return true;
}
private static ParameterizedTypeReference<Object> asParameterizedTypeRef(MethodParameter nestedParam) {
return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType());
}
}

View File

@ -19,9 +19,9 @@ package org.springframework.web.service.invoker;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.codec.multipart.Part;
import org.springframework.lang.Nullable;
@ -89,9 +89,20 @@ public class RequestPartArgumentResolver extends AbstractNamedValueArgumentResol
Class<?> type = parameter.getParameterType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(type);
if (adapter != null) {
Assert.isTrue(!adapter.isNoValue(), "Expected publisher that produces a value");
Publisher<?> publisher = adapter.toPublisher(value);
requestValues.addRequestPart(name, publisher, ResolvableType.forMethodParameter(parameter.nested()));
MethodParameter nestedParameter = parameter.nested();
String message = "Async type for @RequestPart should produce value(s)";
Assert.isTrue(!adapter.isNoValue(), message);
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveValues) {
reactiveValues.addRequestPartPublisher(
name, adapter.toPublisher(value), asParameterizedTypeRef(nestedParameter));
}
else {
throw new IllegalStateException(
"RequestPart with a reactive type is only supported with reactive client");
}
return;
}
}
@ -99,4 +110,8 @@ public class RequestPartArgumentResolver extends AbstractNamedValueArgumentResol
requestValues.addRequestPart(name, value);
}
private static ParameterizedTypeReference<Object> asParameterizedTypeRef(MethodParameter nestedParam) {
return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType());
}
}

View File

@ -125,9 +125,9 @@ class HttpRequestValuesTests {
.build();
@SuppressWarnings("unchecked")
MultiValueMap<String, HttpEntity<?>> map = (MultiValueMap<String, HttpEntity<?>>) requestValues.getBodyValue();
MultiValueMap<String, Object> map = (MultiValueMap<String, Object>) requestValues.getBodyValue();
assertThat(map).hasSize(2);
assertThat(map.getFirst("form field").getBody()).isEqualTo("form value");
assertThat(map.getFirst("form field")).isEqualTo("form value");
assertThat(map.getFirst("entity")).isEqualTo(entity);
}
@ -146,9 +146,9 @@ class HttpRequestValuesTests {
assertThat(uriTemplate).isEqualTo("/path?{queryParam0}={queryParam0[0]}");
@SuppressWarnings("unchecked")
MultiValueMap<String, HttpEntity<?>> map = (MultiValueMap<String, HttpEntity<?>>) requestValues.getBodyValue();
MultiValueMap<String, Object> map = (MultiValueMap<String, Object>) requestValues.getBodyValue();
assertThat(map).hasSize(1);
assertThat(map.getFirst("form field").getBody()).isEqualTo("form value");
assertThat(map.getFirst("form field")).isEqualTo("form value");
}
}

View File

@ -23,6 +23,7 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
@ -32,12 +33,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Unit tests for {@link RequestBodyArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
class RequestBodyArgumentResolverTests {
private final TestExchangeAdapter client = new TestExchangeAdapter();
private final TestReactorExchangeAdapter client = new TestReactorExchangeAdapter();
private final Service service =
HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class);
@ -48,8 +48,8 @@ class RequestBodyArgumentResolverTests {
String body = "bodyValue";
this.service.execute(body);
assertThat(getRequestValues().getBodyValue()).isEqualTo(body);
assertThat(getRequestValues().getBody()).isNull();
assertThat(getBodyValue()).isEqualTo(body);
assertThat(getPublisherBody()).isNull();
}
@Test
@ -57,9 +57,9 @@ class RequestBodyArgumentResolverTests {
Mono<String> bodyMono = Mono.just("bodyValue");
this.service.executeMono(bodyMono);
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBody()).isSameAs(bodyMono);
assertThat(getRequestValues().getBodyElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
assertThat(getBodyValue()).isNull();
assertThat(getPublisherBody()).isSameAs(bodyMono);
assertThat(getBodyElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
}
@Test
@ -68,10 +68,10 @@ class RequestBodyArgumentResolverTests {
String bodyValue = "bodyValue";
this.service.executeSingle(Single.just(bodyValue));
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBodyElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
assertThat(getBodyValue()).isNull();
assertThat(getBodyElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
Publisher<?> body = getRequestValues().getBody();
Publisher<?> body = getPublisherBody();
assertThat(body).isNotNull();
assertThat(((Mono<String>) body).block()).isEqualTo(bodyValue);
}
@ -104,17 +104,32 @@ class RequestBodyArgumentResolverTests {
void ignoreNull() {
this.service.execute(null);
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBody()).isNull();
assertThat(getBodyValue()).isNull();
assertThat(getPublisherBody()).isNull();
this.service.executeMono(null);
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBody()).isNull();
assertThat(getBodyValue()).isNull();
assertThat(getPublisherBody()).isNull();
}
private HttpRequestValues getRequestValues() {
return this.client.getRequestValues();
@Nullable
private Object getBodyValue() {
return getReactiveRequestValues().getBodyValue();
}
@Nullable
private Publisher<?> getPublisherBody() {
return getReactiveRequestValues().getBodyPublisher();
}
@Nullable
private ParameterizedTypeReference<?> getBodyElementType() {
return getReactiveRequestValues().getBodyPublisherElementType();
}
private ReactiveHttpRequestValues getReactiveRequestValues() {
return (ReactiveHttpRequestValues) this.client.getRequestValues();
}

View File

@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class RequestPartArgumentResolverTests {
private final TestExchangeAdapter client = new TestExchangeAdapter();
private final TestReactorExchangeAdapter client = new TestReactorExchangeAdapter();
private final Service service =
HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class);

View File

@ -16,6 +16,7 @@
package org.springframework.web.reactive.function.client.support;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -28,6 +29,7 @@ import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.invoker.AbstractReactorHttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.invoker.ReactiveHttpRequestValues;
import org.springframework.web.service.invoker.ReactorHttpExchangeAdapter;
/**
@ -119,9 +121,13 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
if (requestValues.getBodyValue() != null) {
bodySpec.bodyValue(requestValues.getBodyValue());
}
else if (requestValues.getBody() != null) {
Assert.notNull(requestValues.getBodyElementType(), "Publisher body element type is required");
bodySpec.body(requestValues.getBody(), requestValues.getBodyElementType());
else if (requestValues instanceof ReactiveHttpRequestValues reactiveRequestValues) {
Publisher<?> body = reactiveRequestValues.getBodyPublisher();
if (body != null) {
ParameterizedTypeReference<?> elementType = reactiveRequestValues.getBodyPublisherElementType();
Assert.notNull(elementType, "Publisher body element type is required");
bodySpec.body(body, elementType);
}
}
return bodySpec;