Add resolvers for URI, cookies, and request params

See gh-28386
This commit is contained in:
rstoyanchev 2022-05-02 21:36:09 +01:00
parent f8ac5985bd
commit 2794553d2e
9 changed files with 659 additions and 18 deletions

View File

@ -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.
*
* <p>The argument may be:
* <ul>
* <li>{@code Map<String, ?>} or
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, ?&gt;} with
* multiple cookies and value(s).
* <li>{@code Collection} or an array of cookie values.
* <li>An individual cookie value.
* </ul>
*
* <p>Individual cookie values may be Strings or Objects to be converted to
* String values through the configured {@link ConversionService}.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link CookieValue#required()} is set to {@code false}
* <li>{@link CookieValue#defaultValue()} provides a fallback value
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class CookieValueArgumentResolver extends AbstractNamedValueArgumentResolver {
public CookieValueArgumentResolver(ConversionService conversionService) {
super(conversionService);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
CookieValue annot = parameter.getParameterAnnotation(CookieValue.class);
return (annot == null ? null :
new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "cookie value", true));
}
@Override
protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) {
requestValues.addCookie(name, value);
}
}

View File

@ -18,10 +18,14 @@ package org.springframework.web.service.invoker;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.reactivestreams.Publisher;
@ -29,11 +33,14 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.FormHttpMessageWriter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
@ -75,21 +82,20 @@ public final class HttpRequestValues {
private final ParameterizedTypeReference<?> bodyElementType;
private HttpRequestValues(HttpMethod httpMethod, @Nullable URI uri,
@Nullable String uriTemplate, @Nullable Map<String, String> uriVariables,
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
private HttpRequestValues(HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
HttpHeaders headers, MultiValueMap<String, String> cookies,
@Nullable Object bodyValue,
@Nullable Publisher<?> body,
@Nullable ParameterizedTypeReference<?> bodyElementType) {
@Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> bodyElementType) {
Assert.isTrue(uri == null || uriTemplate == null, "Expected either URI or URI template, not both");
Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template");
this.httpMethod = httpMethod;
this.uri = uri;
this.uriTemplate = (uri != null || uriTemplate != null ? uriTemplate : "");
this.uriVariables = (uriVariables != null ? uriVariables : Collections.emptyMap());
this.headers = (headers != null ? headers : HttpHeaders.EMPTY);
this.cookies = (cookies != null ? cookies : EMPTY_COOKIES_MAP);
this.uriTemplate = uriTemplate;
this.uriVariables = uriVariables;
this.headers = headers;
this.cookies = cookies;
this.bodyValue = bodyValue;
this.body = body;
this.bodyElementType = bodyElementType;
@ -183,6 +189,8 @@ public final class HttpRequestValues {
*/
public final static class Builder {
private static final Function<MultiValueMap<String, String>, byte[]> FORM_DATA_SERIALIZER = new FormDataSerializer();
private HttpMethod httpMethod;
@Nullable
@ -192,7 +200,7 @@ public final class HttpRequestValues {
private String uriTemplate;
@Nullable
private Map<String, String> uriVariables;
private Map<String, String> uriVars;
@Nullable
private HttpHeaders headers;
@ -200,6 +208,9 @@ public final class HttpRequestValues {
@Nullable
private MultiValueMap<String, String> cookies;
@Nullable
private MultiValueMap<String, String> requestParams;
@Nullable
private Object bodyValue;
@ -231,6 +242,7 @@ public final class HttpRequestValues {
public Builder setUri(URI uri) {
this.uri = uri;
this.uriTemplate = null;
this.uriVars = null;
return this;
}
@ -251,8 +263,8 @@ public final class HttpRequestValues {
* {@link #setUri(URI) full URI}.
*/
public Builder setUriVariable(String name, String value) {
this.uriVariables = (this.uriVariables != null ? this.uriVariables : new LinkedHashMap<>());
this.uriVariables.put(name, value);
this.uriVars = (this.uriVars != null ? this.uriVars : new LinkedHashMap<>());
this.uriVars.put(name, value);
this.uri = null;
return this;
}
@ -300,6 +312,21 @@ public final class HttpRequestValues {
return this;
}
/**
* Add the given request parameter name and values.
* <p>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.
* <p>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<String, String> 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<String, String> 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<String, String> uriVars, MultiValueMap<String, String> requestParams) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate);
int i = 0;
for (Map.Entry<String, List<String>> 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<MultiValueMap<String, String>, byte[]> {
@Override
public byte[] apply(MultiValueMap<String, String> requestParams) {
Charset charset = StandardCharsets.UTF_8;
return serializeForm(requestParams, charset).getBytes(charset);
}
}

View File

@ -189,6 +189,9 @@ public final class HttpServiceProxyFactory {
List<HttpServiceArgumentResolver> 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;
}

View File

@ -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;
}
}

View File

@ -27,8 +27,9 @@ import org.springframework.web.bind.annotation.RequestHeader;
*
* <p>The argument may be:
* <ul>
* <li>{@code Map} or {@link org.springframework.util.MultiValueMap} with
* multiple headers and value(s).
* <li>{@code Map<String, ?>} or
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, ?&gt;}
* with multiple headers and value(s).
* <li>{@code Collection} or an array of header values.
* <li>An individual header value.
* </ul>

View File

@ -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.
*
* <p>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.
*
* <p>The argument may be:
* <ul>
* <li>{@code Map<String, ?>} or
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, ?&gt;} with
* multiple request parameter and value(s).
* <li>{@code Collection} or an array of request parameters.
* <li>An individual request parameter.
* </ul>
*
* <p>Individual request parameters may be Strings or Objects to be converted to
* String values through the configured {@link ConversionService}.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link RequestParam#required()} is set to {@code false}
* <li>{@link RequestParam#defaultValue()} provides a fallback value
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @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);
}
}

View File

@ -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<String> 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<Object> multiValueCookie);
@GetExchange
void executeArray(@CookieValue Object... multiValueCookie);
@GetExchange
void executeNamed(@CookieValue(name = "cookieRenamed") String cookie);
@GetExchange
void executeNotRequired(@Nullable @CookieValue(required = false) String cookie);
@GetExchange
void executeWithDefaultValue(@Nullable @CookieValue(defaultValue = "default") String cookie);
@GetExchange
void executeOptional(@CookieValue Optional<Object> cookie);
@GetExchange
void executeOptionalWithDefaultValue(@CookieValue(defaultValue = "default") Optional<Object> cookie);
@GetExchange
void executeMap(@Nullable @CookieValue Map<String, String> cookie);
@GetExchange
void executeMapWithOptionalValue(@CookieValue Map<String, Optional<String>> cookies);
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.junit.jupiter.api.Test;
import org.springframework.web.service.annotation.GetExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link HttpUrlArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class HttpUrlArgumentResolverTests {
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
private final Service service = this.clientAdapter.createService(Service.class);
@Test
void url() {
URI dynamicUrl = URI.create("dynamic-path");
this.service.execute(dynamicUrl);
assertThat(getRequestValues().getUri()).isEqualTo(dynamicUrl);
assertThat(getRequestValues().getUriTemplate()).isNull();
}
private HttpRequestValues getRequestValues() {
return this.clientAdapter.getRequestValues();
}
private interface Service {
@GetExchange("/path")
void execute(URI uri);
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
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.UriComponentsBuilder;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link RequestParamArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class RequestParamArgumentResolverTests {
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
private final Service service = this.clientAdapter.createService(Service.class);
@Test
void formData() {
this.service.postForm("value 1", "value 2");
Object body = this.clientAdapter.getRequestValues().getBodyValue();
assertThat(body).isNotNull().isInstanceOf(byte[].class);
assertThat(new String((byte[]) body, UTF_8)).isEqualTo("param1=value+1&param2=value+2");
}
@Test
void uriTemplate() {
this.service.search("1st value", Arrays.asList("2nd value A", "2nd value B"));
HttpRequestValues requestValues = this.clientAdapter.getRequestValues();
assertThat(requestValues.getUriTemplate())
.isEqualTo("/path?" +
"{queryParam0}={queryParam0[0]}&" +
"{queryParam1}={queryParam1[0]}&" +
"{queryParam1}={queryParam1[1]}");
assertThat(requestValues.getUriVariables())
.containsOnlyKeys("queryParam0", "queryParam1", "queryParam0[0]", "queryParam1[0]", "queryParam1[1]")
.containsEntry("queryParam0", "param1")
.containsEntry("queryParam1", "param2")
.containsEntry("queryParam0[0]", "1st value")
.containsEntry("queryParam1[0]", "2nd value A")
.containsEntry("queryParam1[1]", "2nd value B");
URI uri = UriComponentsBuilder.fromUriString(requestValues.getUriTemplate())
.encode().build(requestValues.getUriVariables());
assertThat(uri.toString())
.isEqualTo("/path?param1=1st%20value&param2=2nd%20value%20A&param2=2nd%20value%20B");
}
@Test
void uri() {
URI baseUrl = URI.create("http://localhost:8080/path");
this.service.searchWithDynamicUri(baseUrl, "1st value", Arrays.asList("2nd value A", "2nd value B"));
assertThat(this.clientAdapter.getRequestValues().getUri().toString())
.isEqualTo(baseUrl + "?param1=1st%20value&param2=2nd%20value%20A&param2=2nd%20value%20B");
}
private interface Service {
@PostExchange(contentType = "application/x-www-form-urlencoded")
void postForm(@RequestParam String param1, @RequestParam String param2);
@GetExchange("/path")
void search(@RequestParam String param1, @RequestParam List<String> param2);
@GetExchange
void searchWithDynamicUri(URI uri, @RequestParam String param1, @RequestParam List<String> param2);
}
}