diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java
new file mode 100644
index 00000000000..d883765c681
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.core.MethodParameter;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.web.bind.annotation.CookieValue;
+
+
+/**
+ * {@link HttpServiceArgumentResolver} for {@link CookieValue @CookieValue}
+ * annotated arguments.
+ *
+ *
The argument may be:
+ *
+ *
{@code Map} or
+ * {@link org.springframework.util.MultiValueMap MultiValueMap<String, ?>} with
+ * multiple cookies and value(s).
+ *
{@code Collection} or an array of cookie values.
+ *
An individual cookie value.
+ *
+ *
+ *
Individual cookie values may be Strings or Objects to be converted to
+ * String values through the configured {@link ConversionService}.
+ *
+ *
If the value is required but {@code null}, {@link IllegalArgumentException}
+ * is raised. The value is not required if:
+ *
+ *
{@link CookieValue#required()} is set to {@code false}
+ *
{@link CookieValue#defaultValue()} provides a fallback value
+ *
The argument is declared as {@link java.util.Optional}
+ *
When {@code "content-type"} is set to
+ * {@code "application/x-www-form-urlencoded"}, request parameters are
+ * encoded in the request body. Otherwise, they are added as URL query
+ * parameters.
+ */
+ public Builder addRequestParameter(String name, String... values) {
+ this.requestParams = (this.requestParams != null ? this.requestParams : new LinkedMultiValueMap<>());
+ for (String value : values) {
+ this.requestParams.add(name, value);
+ }
+ return this;
+ }
+
/**
* Set the request body as a concrete value to be serialized.
*
This is mutually exclusive with, and resets any previously set
@@ -326,10 +353,76 @@ public final class HttpRequestValues {
* Builder the {@link HttpRequestValues} instance.
*/
public HttpRequestValues build() {
+
+ URI uri = this.uri;
+ String uriTemplate = (this.uriTemplate != null || uri != null ? this.uriTemplate : "");
+ Map uriVars = (this.uriVars != null ? new HashMap<>(this.uriVars) : Collections.emptyMap());
+
+ Object bodyValue = this.bodyValue;
+
+ if (!CollectionUtils.isEmpty(this.requestParams)) {
+
+ boolean isFormData = (this.headers != null &&
+ MediaType.APPLICATION_FORM_URLENCODED.equals(this.headers.getContentType()));
+
+ if (isFormData) {
+ Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request params, not both");
+ bodyValue = FORM_DATA_SERIALIZER.apply(this.requestParams);
+ }
+ else if (uri != null) {
+ uri = UriComponentsBuilder.fromUri(uri)
+ .queryParams(UriUtils.encodeQueryParams(this.requestParams))
+ .build(true)
+ .toUri();
+ }
+ else {
+ uriVars = (uriVars.isEmpty() ? new HashMap<>() : uriVars);
+ uriTemplate = appendQueryParams(uriTemplate, uriVars, this.requestParams);
+ }
+ }
+
+ HttpHeaders headers = HttpHeaders.EMPTY;
+ if (this.headers != null) {
+ headers = new HttpHeaders();
+ headers.putAll(this.headers);
+ }
+
+ MultiValueMap cookies = (this.cookies != null ?
+ new LinkedMultiValueMap<>(this.cookies) : EMPTY_COOKIES_MAP);
+
return new HttpRequestValues(
- this.httpMethod, this.uri, this.uriTemplate, this.uriVariables,
- this.headers, this.cookies,
- this.bodyValue, this.body, this.bodyElementType);
+ this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, bodyValue,
+ this.body, this.bodyElementType);
+ }
+
+ private String appendQueryParams(
+ String uriTemplate, Map uriVars, MultiValueMap requestParams) {
+
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate);
+ int i = 0;
+ for (Map.Entry> entry : requestParams.entrySet()) {
+ String nameVar = "queryParam" + i;
+ uriVars.put(nameVar, entry.getKey());
+ for (int j = 0; j < entry.getValue().size(); j++) {
+ String valueVar = nameVar + "[" + j + "]";
+ uriVars.put(valueVar, entry.getValue().get(j));
+ uriComponentsBuilder.queryParam("{" + nameVar + "}", "{" + valueVar + "}");
+ }
+ i++;
+ }
+ return uriComponentsBuilder.build().toUriString();
+ }
+
+ }
+
+
+ private static class FormDataSerializer
+ extends FormHttpMessageWriter implements Function, byte[]> {
+
+ @Override
+ public byte[] apply(MultiValueMap requestParams) {
+ Charset charset = StandardCharsets.UTF_8;
+ return serializeForm(requestParams, charset).getBytes(charset);
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
index e4fcb5e4af8..cebb149f862 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
@@ -189,6 +189,9 @@ public final class HttpServiceProxyFactory {
List resolvers = new ArrayList<>(this.customResolvers);
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
resolvers.add(new PathVariableArgumentResolver(conversionService));
+ resolvers.add(new CookieValueArgumentResolver(conversionService));
+ resolvers.add(new RequestParamArgumentResolver(conversionService));
+ resolvers.add(new HttpUrlArgumentResolver());
resolvers.add(new HttpMethodArgumentResolver());
return resolvers;
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java
new file mode 100644
index 00000000000..7b65280b823
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.core.MethodParameter;
+import org.springframework.http.HttpMethod;
+import org.springframework.lang.Nullable;
+
+
+/**
+ * {@link HttpServiceArgumentResolver} that resolves the target
+ * request's URL from an {@link HttpMethod} argument.
+ *
+ * @author Rossen Stoyanchev
+ * @since 6.0
+ */
+public class HttpUrlArgumentResolver implements HttpServiceArgumentResolver {
+
+ @Override
+ public boolean resolve(
+ @Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
+
+ if (argument instanceof URI uri) {
+ requestValues.setUri(uri);
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java
index 58928329bf9..92691c0a0f9 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java
@@ -27,8 +27,9 @@ import org.springframework.web.bind.annotation.RequestHeader;
*
*
The argument may be:
*
- *
{@code Map} or {@link org.springframework.util.MultiValueMap} with
- * multiple headers and value(s).
+ *
{@code Map} or
+ * {@link org.springframework.util.MultiValueMap MultiValueMap<String, ?>}
+ * with multiple headers and value(s).
*
{@code Collection} or an array of header values.
*
An individual header value.
*
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
new file mode 100644
index 00000000000..ac414c6910e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.core.MethodParameter;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.web.bind.annotation.RequestParam;
+
+
+/**
+ * {@link HttpServiceArgumentResolver} for {@link RequestParam @RequestParam}
+ * annotated arguments.
+ *
+ *
When {@code "content-type"} is set to
+ * {@code "application/x-www-form-urlencoded"}, request parameters are encoded
+ * in the request body. Otherwise, they are added as URL query parameters.
+ *
+ *
The argument may be:
+ *
+ *
{@code Map} or
+ * {@link org.springframework.util.MultiValueMap MultiValueMap<String, ?>} with
+ * multiple request parameter and value(s).
+ *
{@code Collection} or an array of request parameters.
+ *
An individual request parameter.
+ *
+ *
+ *
Individual request parameters may be Strings or Objects to be converted to
+ * String values through the configured {@link ConversionService}.
+ *
+ *
If the value is required but {@code null}, {@link IllegalArgumentException}
+ * is raised. The value is not required if:
+ *
+ *
{@link RequestParam#required()} is set to {@code false}
+ *
{@link RequestParam#defaultValue()} provides a fallback value
+ *
The argument is declared as {@link java.util.Optional}
+ *
+ *
+ * @author Rossen Stoyanchev
+ * @since 6.0
+ */
+public class RequestParamArgumentResolver extends AbstractNamedValueArgumentResolver {
+
+
+ public RequestParamArgumentResolver(ConversionService conversionService) {
+ super(conversionService);
+ }
+
+
+ @Override
+ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
+ RequestParam annot = parameter.getParameterAnnotation(RequestParam.class);
+ return (annot == null ? null :
+ new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "request parameter", true));
+ }
+
+ @Override
+ protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) {
+ requestValues.addRequestParameter(name, value);
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java
new file mode 100644
index 00000000000..0ff41756b0a
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2002-2022 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.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.groovy.util.Maps;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.bind.annotation.CookieValue;
+import org.springframework.web.service.annotation.GetExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+
+/**
+ * Unit tests for {@link RequestHeaderArgumentResolver}.
+ *
+ * @author Rossen Stoyanchev
+ */
+class CookieValueArgumentResolverTests {
+
+ private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
+
+ private final Service service = this.clientAdapter.createService(Service.class);
+
+
+ @Test
+ void stringCookie() {
+ this.service.executeString("test");
+ assertCookie("cookie", "test");
+ }
+
+ @Test
+ void objectCookie() {
+ this.service.execute(Boolean.TRUE);
+ assertCookie("cookie", "true");
+ }
+
+ @Test
+ void listCookie() {
+ this.service.executeList(List.of("test1", Boolean.TRUE, "test3"));
+ assertCookie("multiValueCookie", "test1", "true", "test3");
+ }
+
+ @Test
+ void arrayCookie() {
+ this.service.executeArray("test1", Boolean.FALSE, "test3");
+ assertCookie("multiValueCookie", "test1", "false", "test3");
+ }
+
+ @Test
+ void namedCookie() {
+ this.service.executeNamed("test");
+ assertCookie("cookieRenamed", "test");
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Test
+ void nullCookieRequired() {
+ assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeString(null));
+ }
+
+ @Test
+ void nullCookieNotRequired() {
+ this.service.executeNotRequired(null);
+ assertCookie("cookie");
+ }
+
+ @Test
+ void nullCookieWithDefaultValue() {
+ this.service.executeWithDefaultValue(null);
+ assertCookie("cookie", "default");
+ }
+
+ @Test
+ void optionalStringCookie() {
+ this.service.executeOptional(Optional.of("test"));
+ assertCookie("cookie", "test");
+ }
+
+ @Test
+ void optionalObjectCookie() {
+ this.service.executeOptional(Optional.of(Boolean.TRUE));
+ assertCookie("cookie", "true");
+ }
+
+ @Test
+ void optionalEmpty() {
+ this.service.executeOptional(Optional.empty());
+ assertCookie("cookie");
+ }
+
+ @Test
+ void optionalEmpthyWithDefaultValue() {
+ this.service.executeOptionalWithDefaultValue(Optional.empty());
+ assertCookie("cookie", "default");
+ }
+
+ @Test
+ void mapOfCookies() {
+ this.service.executeMap(Maps.of("cookie1", "true", "cookie2", "false"));
+ assertCookie("cookie1", "true");
+ assertCookie("cookie2", "false");
+ }
+
+ @Test
+ void mapOfCookiesIsNull() {
+ assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeMap(null));
+ }
+
+ @Test
+ void mapOfCookiesHasOptionalValue() {
+ this.service.executeMapWithOptionalValue(Map.of("cookie", Optional.of("test")));
+ assertCookie("cookie", "test");
+ }
+
+ private void assertCookie(String key, String... values) {
+ List actualValues = this.clientAdapter.getRequestValues().getCookies().get(key);
+ if (ObjectUtils.isEmpty(values)) {
+ assertThat(actualValues).isNull();
+ }
+ else {
+ assertThat(actualValues).containsOnly(values);
+ }
+ }
+
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private interface Service {
+
+ @GetExchange
+ void executeString(@CookieValue String cookie);
+
+ @GetExchange
+ void execute(@CookieValue Object cookie);
+
+ @GetExchange
+ void executeList(@CookieValue List