diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 1f590c46..a063e212 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -220,6 +220,12 @@ 3.4.0 test + + io.vavr + vavr + 0.10.0 + test + diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java index 3c7761cf..87ab9ce7 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java @@ -45,6 +45,7 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -129,8 +130,8 @@ public class SpringMvcContract extends Contract.BaseContract // Feign applies the Param.Expander to each element of an Iterable, so in those // cases we need to provide a TypeDescriptor of the element. if (typeDescriptor.isAssignableTo(ITERABLE_TYPE_DESCRIPTOR)) { - TypeDescriptor elementTypeDescriptor = typeDescriptor - .getElementTypeDescriptor(); + TypeDescriptor elementTypeDescriptor = getElementTypeDescriptor( + typeDescriptor); checkState(elementTypeDescriptor != null, "Could not resolve element type of Iterable type %s. Not declared?", @@ -141,6 +142,22 @@ public class SpringMvcContract extends Contract.BaseContract return typeDescriptor; } + private static TypeDescriptor getElementTypeDescriptor( + TypeDescriptor typeDescriptor) { + TypeDescriptor elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor(); + // that means it's not a collection but it is iterable, gh-135 + if (elementTypeDescriptor == null + && Iterable.class.isAssignableFrom(typeDescriptor.getType())) { + ResolvableType type = typeDescriptor.getResolvableType().as(Iterable.class) + .getGeneric(0); + if (type.resolve() == null) { + return null; + } + return new TypeDescriptor(type, null, typeDescriptor.getAnnotations()); + } + return elementTypeDescriptor; + } + @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/valid/IterableParameterTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/valid/IterableParameterTests.java new file mode 100644 index 00000000..c298c8ce --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/valid/IterableParameterTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2019 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 + * + * http://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.cloud.openfeign.valid; + +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.cloud.netflix.ribbon.StaticServerList; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.test.NoSecurityConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Spencer Gibb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = IterableParameterTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { + "spring.application.name=iterableparametertest", + "logging.level.org.springframework.cloud.openfeign.valid=DEBUG", + "feign.httpclient.enabled=false", "feign.okhttp.enabled=false", + "feign.hystrix.enabled=false" }) +@DirtiesContext +public class IterableParameterTests { + + @Autowired + private TestClient testClient; + + @Test + public void testClient() { + assertThat(this.testClient).as("testClient was null").isNotNull(); + String results = this.testClient.echo(HashSet.of("a", "b")); + assertThat(results).isEqualTo("a,b"); + } + + @FeignClient(name = "localapp") + protected interface TestClient { + + @GetMapping("/echo") + String echo(@RequestParam Set set); + + } + + @Configuration + @EnableAutoConfiguration + @RestController + @EnableFeignClients(clients = TestClient.class) + @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class) + @Import(NoSecurityConfiguration.class) + protected static class Application { + + @GetMapping("/echo") + public String echo(@RequestParam String set) { + return set; + } + + } + + // Load balancer with fixed server list for "local" pointing to localhost + public static class LocalRibbonClientConfiguration { + + @LocalServerPort + private int port = 0; + + @Bean + public ServerList ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } + + } + +} diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 2bd6edec..e0aac3a4 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -9,6 +9,7 @@ +