Browse Source

Add ReactiveHttpRequestValues

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

Closes gh-30117
pull/30869/head
rstoyanchev 3 years ago
parent
commit
a3e37597aa
  1. 121
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java
  2. 35
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
  3. 275
      spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java
  4. 15
      spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java
  5. 23
      spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java
  6. 8
      spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java
  7. 47
      spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java
  8. 2
      spring-web/src/test/java/org/springframework/web/service/invoker/RequestPartArgumentResolverTests.java
  9. 12
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java

121
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.ParameterizedTypeReference;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
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;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -48,7 +48,7 @@ import org.springframework.web.util.UriUtils;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 6.0 * @since 6.0
*/ */
public final class HttpRequestValues { public class HttpRequestValues {
private static final MultiValueMap<String, String> EMPTY_COOKIES_MAP = private static final MultiValueMap<String, String> EMPTY_COOKIES_MAP =
CollectionUtils.toMultiValueMap(Collections.emptyMap()); CollectionUtils.toMultiValueMap(Collections.emptyMap());
@ -74,18 +74,11 @@ public final class HttpRequestValues {
@Nullable @Nullable
private final Object bodyValue; private final Object bodyValue;
@Nullable
private final Publisher<?> body;
@Nullable
private final ParameterizedTypeReference<?> bodyElementType;
protected HttpRequestValues(@Nullable HttpMethod httpMethod,
private HttpRequestValues(@Nullable HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables, @Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes, HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
@Nullable Object bodyValue, @Nullable Object bodyValue) {
@Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> bodyElementType) {
Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template"); Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template");
@ -97,8 +90,6 @@ public final class HttpRequestValues {
this.cookies = cookies; this.cookies = cookies;
this.attributes = attributes; this.attributes = attributes;
this.bodyValue = bodyValue; 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. * 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 @Nullable
public Object getBodyValue() { public Object getBodyValue() {
@ -173,18 +162,24 @@ public final class HttpRequestValues {
* Return the request body as a Publisher. * Return the request body as a Publisher.
* <p>This is mutually exclusive with {@link #getBodyValue()}. * <p>This is mutually exclusive with {@link #getBodyValue()}.
* Only one of the two or neither is set. * 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 @Nullable
public Publisher<?> getBody() { 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 @Nullable
public ParameterizedTypeReference<?> getBodyElementType() { public ParameterizedTypeReference<?> getBodyElementType() {
return this.bodyElementType; throw new UnsupportedOperationException();
} }
@ -196,7 +191,7 @@ public final class HttpRequestValues {
/** /**
* Builder for {@link HttpRequestValues}. * Builder for {@link HttpRequestValues}.
*/ */
public final static class Builder { public static class Builder {
@Nullable @Nullable
private HttpMethod httpMethod; private HttpMethod httpMethod;
@ -220,7 +215,7 @@ public final class HttpRequestValues {
private MultiValueMap<String, String> requestParams; private MultiValueMap<String, String> requestParams;
@Nullable @Nullable
private MultipartBodyBuilder multipartBuilder; private MultiValueMap<String, Object> parts;
@Nullable @Nullable
private Map<String, Object> attributes; private Map<String, Object> attributes;
@ -228,12 +223,6 @@ public final class HttpRequestValues {
@Nullable @Nullable
private Object bodyValue; private Object bodyValue;
@Nullable
private Publisher<?> body;
@Nullable
private ParameterizedTypeReference<?> bodyElementType;
/** /**
* Set the HTTP method for the request. * 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 * Add a part for a multipart request. The part may be:
* in {@link MultipartBodyBuilder#part(String, Object)}. * <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) { public Builder addRequestPart(String name, Object part) {
this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder()); this.parts = (this.parts != null ? this.parts : new LinkedMultiValueMap<>());
this.multipartBuilder.part(name, part); this.parts.add(name, part);
return this; return this;
} }
/** /**
* Variant of {@link #addRequestPart(String, Object)} that allows the * Variant of {@link #addRequestPart(String, Object)} that allows the
* part value to be produced by a {@link Publisher}. * 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) { public <T, P extends Publisher<T>> Builder addRequestPart(String name, P publisher, ResolvableType type) {
this.multipartBuilder = (this.multipartBuilder != null ? this.multipartBuilder : new MultipartBodyBuilder()); throw new UnsupportedOperationException();
this.multipartBuilder.asyncPart(name, publisher, ParameterizedTypeReference.forType(type.getType()));
return this;
} }
/** /**
@ -358,25 +354,22 @@ public final class HttpRequestValues {
} }
/** /**
* Set the request body as a concrete value to be serialized. * Set the request body as an Object to be serialized.
* <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setBody(Publisher, ParameterizedTypeReference) body Publisher}.
*/ */
public void setBodyValue(Object bodyValue) { public void setBodyValue(Object bodyValue) {
this.bodyValue = 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 * <p>This is mutually exclusive with, and resets any previously set
* {@linkplain #setBodyValue(Object) body value}. * {@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) { public <T, P extends Publisher<T>> void setBody(P body, ParameterizedTypeReference<T> elementTye) {
this.body = body; throw new UnsupportedOperationException();
this.bodyElementType = elementTye;
this.bodyValue = null;
} }
/** /**
@ -389,15 +382,15 @@ public final class HttpRequestValues {
Map<String, String> uriVars = (this.uriVars != null ? new HashMap<>(this.uriVars) : Collections.emptyMap()); Map<String, String> uriVars = (this.uriVars != null ? new HashMap<>(this.uriVars) : Collections.emptyMap());
Object bodyValue = this.bodyValue; Object bodyValue = this.bodyValue;
if (this.multipartBuilder != null) { if (hasParts()) {
Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request parts, not both"); Assert.isTrue(!hasBody(), "Expected body or request parts, not both");
bodyValue = this.multipartBuilder.build(); bodyValue = buildMultipartBody();
} }
if (!CollectionUtils.isEmpty(this.requestParams)) { if (!CollectionUtils.isEmpty(this.requestParams)) {
if (hasContentType(MediaType.APPLICATION_FORM_URLENCODED)) { if (hasFormDataContentType()) {
Assert.isTrue(this.multipartBuilder == null, "Cannot add parts to form data request"); Assert.isTrue(!hasParts(), "Request parts not expected for a form data request");
Assert.isTrue(bodyValue == null && this.body == null, "Cannot set body of form data request"); Assert.isTrue(!hasBody(), "Body not expected for a form data request");
bodyValue = new LinkedMultiValueMap<>(this.requestParams); bodyValue = new LinkedMultiValueMap<>(this.requestParams);
} }
else if (uri != null) { else if (uri != null) {
@ -426,13 +419,26 @@ public final class HttpRequestValues {
Map<String, Object> attributes = (this.attributes != null ? Map<String, Object> attributes = (this.attributes != null ?
new HashMap<>(this.attributes) : Collections.emptyMap()); new HashMap<>(this.attributes) : Collections.emptyMap());
return new HttpRequestValues( return createRequestValues(
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, bodyValue);
bodyValue, this.body, this.bodyElementType); }
protected boolean hasParts() {
return (this.parts != null);
}
protected boolean hasBody() {
return (this.bodyValue != null);
} }
private boolean hasContentType(MediaType mediaType) { protected Object buildMultipartBody() {
return (this.headers != null && mediaType.equals(this.headers.getContentType())); 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( private String appendQueryParams(
@ -453,6 +459,15 @@ public final class HttpRequestValues {
return uriComponentsBuilder.build().toUriString(); 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);
}
} }
} }

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

275
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<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);
}
}
}

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

23
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.reactivestreams.Publisher;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.codec.multipart.Part; import org.springframework.http.codec.multipart.Part;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -89,9 +89,20 @@ public class RequestPartArgumentResolver extends AbstractNamedValueArgumentResol
Class<?> type = parameter.getParameterType(); Class<?> type = parameter.getParameterType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(type); ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(type);
if (adapter != null) { if (adapter != null) {
Assert.isTrue(!adapter.isNoValue(), "Expected publisher that produces a value"); MethodParameter nestedParameter = parameter.nested();
Publisher<?> publisher = adapter.toPublisher(value);
requestValues.addRequestPart(name, publisher, ResolvableType.forMethodParameter(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; return;
} }
} }
@ -99,4 +110,8 @@ public class RequestPartArgumentResolver extends AbstractNamedValueArgumentResol
requestValues.addRequestPart(name, value); requestValues.addRequestPart(name, value);
} }
private static ParameterizedTypeReference<Object> asParameterizedTypeRef(MethodParameter nestedParam) {
return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType());
}
} }

8
spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java

@ -125,9 +125,9 @@ class HttpRequestValuesTests {
.build(); .build();
@SuppressWarnings("unchecked") @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).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); assertThat(map.getFirst("entity")).isEqualTo(entity);
} }
@ -146,9 +146,9 @@ class HttpRequestValuesTests {
assertThat(uriTemplate).isEqualTo("/path?{queryParam0}={queryParam0[0]}"); assertThat(uriTemplate).isEqualTo("/path?{queryParam0}={queryParam0[0]}");
@SuppressWarnings("unchecked") @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).hasSize(1);
assertThat(map.getFirst("form field").getBody()).isEqualTo("form value"); assertThat(map.getFirst("form field")).isEqualTo("form value");
} }
} }

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

2
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 { class RequestPartArgumentResolverTests {
private final TestExchangeAdapter client = new TestExchangeAdapter(); private final TestReactorExchangeAdapter client = new TestReactorExchangeAdapter();
private final Service service = private final Service service =
HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class); HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class);

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

Loading…
Cancel
Save