diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java index cf8c7ed6a61..de8ad19ce2a 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java @@ -129,7 +129,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HttpServiceA */ @Nullable protected NamedValueInfo createNamedValueInfo( - MethodParameter parameter, HttpRequestValues.Metadata requestValues) { + MethodParameter parameter, HttpRequestValues.Metadata metadata) { return createNamedValueInfo(parameter); } @@ -246,6 +246,8 @@ public abstract class AbstractNamedValueArgumentResolver implements HttpServiceA * @param required whether it is marked as required * @param defaultValue fallback value, possibly {@link ValueConstants#DEFAULT_NONE} * @param label how it should appear in error messages, e.g. "path variable", "request header" + * @param multiValued whether this argument resolver supports sending multiple values; + * if not, then multiple values are formatted as a String value */ public NamedValueInfo( String name, boolean required, @Nullable String defaultValue, String label, boolean multiValued) { diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java index 03fdbf12466..019be4aa968 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java @@ -55,61 +55,79 @@ import org.springframework.web.bind.annotation.RequestParam; */ public class RequestParamArgumentResolver extends AbstractNamedValueArgumentResolver { - private boolean formatAsSingleValue = true; + private boolean favorSingleValue; public RequestParamArgumentResolver(ConversionService conversionService) { super(conversionService); } - public RequestParamArgumentResolver(ConversionService conversionService, boolean formatAsSingleValue) { - super(conversionService); - this.formatAsSingleValue = formatAsSingleValue; - } - - @Override - @Nullable - protected NamedValueInfo createNamedValueInfo(MethodParameter parameter, HttpRequestValues.Metadata requestValues) { - MediaType contentType = requestValues.getContentType(); - if (contentType != null && isMultiValueFormContentType(contentType)) { - this.formatAsSingleValue = true; - } + /** + * Whether to format multiple values (e.g. collection, array) as a single + * String value through the configured {@link ConversionService} unless the + * content type is form data, or it is a multipart request. + *
By default, this is {@code false} in which case formatting is not applied,
+ * and a separate parameter with the same name is created for each value.
+ * @since 6.2
+ */
+ public void setFavorSingleValue(boolean favorSingleValue) {
+ this.favorSingleValue = favorSingleValue;
+ }
- return createNamedValueInfo(parameter);
+ /**
+ * Return the setting for {@link #setFavorSingleValue favorSingleValue}.
+ * @since 6.2
+ */
+ public boolean isFavorSingleValue() {
+ return this.favorSingleValue;
}
+
@Override
@Nullable
- protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
+ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter, HttpRequestValues.Metadata metadata) {
RequestParam annot = parameter.getParameterAnnotation(RequestParam.class);
if (annot == null) {
return null;
}
-
- return (annot == null ? null :
- new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(),
- "request parameter", this.formatAsSingleValue));
+ return new NamedValueInfo(
+ annot.name(), annot.required(), annot.defaultValue(), "request parameter",
+ supportsMultipleValues(parameter, metadata));
}
@Override
- protected void addRequestValue(
- String name, Object value, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
-
- requestValues.addRequestParameter(name, (String) value);
+ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
+ // Shouldn't be called since we override createNamedValueInfo with HttpRequestValues.Metadata
+ throw new UnsupportedOperationException();
}
- protected boolean isFormatAsSingleValue() {
- return this.formatAsSingleValue;
+ /**
+ * Determine whether the resolver should send multi-value request parameters
+ * as individual values. If not, they are formatted to a single String value.
+ * The default implementation uses {@link #isFavorSingleValue()} to decide
+ * unless the content type is form data, or it is a multipart request.
+ * @since 6.2
+ */
+ protected boolean supportsMultipleValues(MethodParameter parameter, HttpRequestValues.Metadata metadata) {
+ return (!isFavorSingleValue() || isFormOrMultipartContent(metadata));
}
- protected void setFormatAsSingleValue(boolean formatAsSingleValue) {
- this.formatAsSingleValue = formatAsSingleValue;
+ /**
+ * Whether the content type is form data, or it is a multipart request.
+ * @since 6.2
+ */
+ protected boolean isFormOrMultipartContent(HttpRequestValues.Metadata metadata) {
+ MediaType mediaType = metadata.getContentType();
+ return (mediaType != null && (mediaType.getType().equals("multipart") ||
+ mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)));
}
- protected boolean isMultiValueFormContentType(MediaType contentType) {
- return contentType.equals(MediaType.APPLICATION_FORM_URLENCODED)
- || contentType.getType().equals("multipart");
+ @Override
+ protected void addRequestValue(
+ String name, Object value, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
+
+ requestValues.addRequestParameter(name, (String) value);
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java
index f920f90cbbe..d68d725bdb1 100644
--- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java
@@ -16,17 +16,18 @@
package org.springframework.web.service.invoker;
-import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.junit.jupiter.api.Test;
-import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.PostExchange;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
@@ -62,24 +63,18 @@ class RequestParamArgumentResolverTests {
}
@Test
- @SuppressWarnings("unchecked")
void requestParamWithDisabledFormattingCollectionValue() {
- ConversionService conversionService = new DefaultConversionService();
- boolean formatAsSingleValue = false;
- Service service = builder.customArgumentResolver(
- new RequestParamArgumentResolver(conversionService, formatAsSingleValue))
- .build()
- .createClient(Service.class);
- List