From a3e37597aaeece84b473194b2feb0a3e225a1a4d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 11 Jul 2023 07:42:04 +0100 Subject: [PATCH] Add ReactiveHttpRequestValues Separate collection and handling of reactive request values into a subclass of HttpRequestValues. Closes gh-30117 --- .../service/invoker/HttpRequestValues.java | 121 ++++---- .../service/invoker/HttpServiceMethod.java | 37 ++- .../invoker/ReactiveHttpRequestValues.java | 275 ++++++++++++++++++ .../invoker/RequestBodyArgumentResolver.java | 15 +- .../invoker/RequestPartArgumentResolver.java | 23 +- .../invoker/HttpRequestValuesTests.java | 8 +- .../RequestBodyArgumentResolverTests.java | 47 ++- .../RequestPartArgumentResolverTests.java | 2 +- .../client/support/WebClientAdapter.java | 12 +- 9 files changed, 436 insertions(+), 104 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index a58d639f8e2..85a75d34a73 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -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 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 uriVariables, HttpHeaders headers, MultiValueMap cookies, Map 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. - *

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. *

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 requestParams; @Nullable - private MultipartBodyBuilder multipartBuilder; + private MultiValueMap parts; @Nullable private Map 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: + *

    + *
  • String -- form field + *
  • {@link org.springframework.core.io.Resource Resource} -- file part + *
  • Object -- content to be encoded (e.g. to JSON) + *
  • {@link HttpEntity} -- part content and headers although generally it's + * easier to add headers through the returned builder + *
*/ 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 > 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. - *

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. *

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 > void setBody(P body, ParameterizedTypeReference elementTye) { - this.body = body; - this.bodyElementType = elementTye; - this.bodyValue = null; + throw new UnsupportedOperationException(); } /** @@ -389,15 +382,15 @@ public final class HttpRequestValues { Map 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 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 uriVars, + HttpHeaders headers, MultiValueMap cookies, Map attributes, + @Nullable Object bodyValue) { + + return new HttpRequestValues( + this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, bodyValue); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index 18fb67fc9b6..c532e132b93 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -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 acceptMediaTypes) { - - private HttpRequestValuesInitializer( - HttpMethod httpMethod, @Nullable String url, - @Nullable MediaType contentType, @Nullable List acceptMediaTypes) { - - this.url = url; - this.httpMethod = httpMethod; - this.contentType = contentType; - this.acceptMediaTypes = acceptMediaTypes; - } + @Nullable MediaType contentType, @Nullable List acceptMediaTypes, + Supplier 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 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 acceptableMediaTypes = initAccept(annot1, annot2); - return new HttpRequestValuesInitializer(httpMethod, url, contentType, acceptableMediaTypes); + return new HttpRequestValuesInitializer( + httpMethod, url, contentType, acceptableMediaTypes, requestValuesSupplier); } @Nullable diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java new file mode 100644 index 00000000000..9a6f3410409 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -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 uriVariables, + HttpHeaders headers, MultiValueMap cookies, Map 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. + *

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. + *

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 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 > Builder addRequestPartPublisher( + String name, P publisher, ParameterizedTypeReference elementTye) { + + this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder()); + this.multipartBuilder.asyncPart(name, publisher, elementTye); + return this; + } + + @SuppressWarnings("removal") + @Override + public > Builder addRequestPart(String name, P publisher, ResolvableType type) { + return addRequestPartPublisher(name, publisher, ParameterizedTypeReference.forType(type.getType())); + } + + /** + * {@inheritDoc} + *

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. + *

This is mutually exclusive with, and resets any previously set + * {@linkplain #setBodyValue(Object) body value}. + */ + @SuppressWarnings("DataFlowIssue") + public > void setBodyPublisher(P body, ParameterizedTypeReference elementTye) { + this.body = body; + this.bodyElementType = elementTye; + super.setBodyValue(null); + } + + @SuppressWarnings("removal") + @Override + public > void setBody(P body, ParameterizedTypeReference 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 uriVars, + HttpHeaders headers, MultiValueMap cookies, Map attributes, + @Nullable Object bodyValue) { + + return new ReactiveHttpRequestValues( + httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, + bodyValue, this.body, this.bodyElementType); + } + + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java index 01246a3cf81..cdd3c6f0c05 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java @@ -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 asParameterizedTypeRef(MethodParameter nestedParam) { + return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType()); + } + } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java index 65a147e1d6f..f9b14c59c47 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java @@ -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 asParameterizedTypeRef(MethodParameter nestedParam) { + return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType()); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java index 39aa9e6ac84..40692cc27ae 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java @@ -125,9 +125,9 @@ class HttpRequestValuesTests { .build(); @SuppressWarnings("unchecked") - MultiValueMap> map = (MultiValueMap>) requestValues.getBodyValue(); + MultiValueMap map = (MultiValueMap) 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> map = (MultiValueMap>) requestValues.getBodyValue(); + MultiValueMap map = (MultiValueMap) requestValues.getBodyValue(); assertThat(map).hasSize(1); - assertThat(map.getFirst("form field").getBody()).isEqualTo("form value"); + assertThat(map.getFirst("form field")).isEqualTo("form value"); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java index ca41cead540..318214255f3 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java @@ -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 bodyMono = Mono.just("bodyValue"); this.service.executeMono(bodyMono); - assertThat(getRequestValues().getBodyValue()).isNull(); - assertThat(getRequestValues().getBody()).isSameAs(bodyMono); - assertThat(getRequestValues().getBodyElementType()).isEqualTo(new ParameterizedTypeReference() {}); + assertThat(getBodyValue()).isNull(); + assertThat(getPublisherBody()).isSameAs(bodyMono); + assertThat(getBodyElementType()).isEqualTo(new ParameterizedTypeReference() {}); } @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() {}); + assertThat(getBodyValue()).isNull(); + assertThat(getBodyElementType()).isEqualTo(new ParameterizedTypeReference() {}); - Publisher body = getRequestValues().getBody(); + Publisher body = getPublisherBody(); assertThat(body).isNotNull(); assertThat(((Mono) 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(); } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java index c1d28c98c51..228647cd37b 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java @@ -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); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java index 5a058b06081..3c43c2be270 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java @@ -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;