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 ca72b532bcac..03fdbf124662 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -18,6 +18,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.RequestParam; @@ -54,18 +55,41 @@ */ public class RequestParamArgumentResolver extends AbstractNamedValueArgumentResolver { + private boolean formatAsSingleValue = true; + 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; + } + + return createNamedValueInfo(parameter); + } @Override @Nullable protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { 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", true)); + new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), + "request parameter", this.formatAsSingleValue)); } @Override @@ -75,4 +99,17 @@ protected void addRequestValue( requestValues.addRequestParameter(name, (String) value); } + protected boolean isFormatAsSingleValue() { + return this.formatAsSingleValue; + } + + protected void setFormatAsSingleValue(boolean formatAsSingleValue) { + this.formatAsSingleValue = formatAsSingleValue; + } + + protected boolean isMultiValueFormContentType(MediaType contentType) { + return contentType.equals(MediaType.APPLICATION_FORM_URLENCODED) + || contentType.getType().equals("multipart"); + } + } 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 f6cd274d01e9..f920f90cbbe2 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,12 +16,16 @@ package org.springframework.web.service.invoker; +import java.util.HashMap; import java.util.List; 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 static org.assertj.core.api.Assertions.assertThat; @@ -41,14 +45,14 @@ class RequestParamArgumentResolverTests { private final TestExchangeAdapter client = new TestExchangeAdapter(); - private final Service service = - HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class); + private final HttpServiceProxyFactory.Builder builder = HttpServiceProxyFactory.builderFor(this.client); @Test @SuppressWarnings("unchecked") void requestParam() { - this.service.postForm("value 1", "value 2"); + Service service = builder.build().createClient(Service.class); + service.postForm("value 1", "value 2"); Object body = this.client.getRequestValues().getBodyValue(); assertThat(body).isInstanceOf(MultiValueMap.class); @@ -57,12 +61,34 @@ void requestParam() { .containsEntry("param2", List.of("value 2")); } + @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 collectionParams = List.of("1", "2", "3"); + service.getForm("value 1", collectionParams); + + Object uriVariables = this.client.getRequestValues().getUriVariables(); + assertThat(uriVariables).isNotInstanceOf(MultiValueMap.class).isInstanceOf(HashMap.class); + assertThat((HashMap) uriVariables).hasSize(4) + .containsEntry("queryParam0", "param1") + .containsEntry("queryParam0[0]", "value 1") + .containsEntry("queryParam1", "param2") + .containsEntry("queryParam1[0]", String.join(",", collectionParams)); + } private interface Service { @PostExchange(contentType = "application/x-www-form-urlencoded") void postForm(@RequestParam String param1, @RequestParam String param2); + @GetExchange + void getForm(@RequestParam String param1, @RequestParam List param2); } }