diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java index 883ba736..67a11c46 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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. @@ -33,6 +33,9 @@ import org.springframework.cloud.netflix.feign.support.SpringMvcContract; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import com.netflix.hystrix.HystrixCommand; @@ -56,6 +59,9 @@ public class FeignClientsConfiguration { @Autowired(required = false) private List parameterProcessors = new ArrayList<>(); + @Autowired(required = false) + private List feignFormatterRegistrars = new ArrayList<>(); + @Bean @ConditionalOnMissingBean public Decoder feignDecoder() { @@ -70,8 +76,17 @@ public class FeignClientsConfiguration { @Bean @ConditionalOnMissingBean - public Contract feignContract() { - return new SpringMvcContract(this.parameterProcessors); + public Contract feignContract(ConversionService feignConversionService) { + return new SpringMvcContract(this.parameterProcessors, feignConversionService); + } + + @Bean + public FormattingConversionService feignConversionService() { + FormattingConversionService conversionService = new DefaultFormattingConversionService(); + for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) { + feignFormatterRegistrar.registerFormatters(conversionService); + } + return conversionService; } @Configuration diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignFormatterRegistrar.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignFormatterRegistrar.java new file mode 100644 index 00000000..0a6f0e93 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignFormatterRegistrar.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2016 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.netflix.feign; + +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.support.FormattingConversionService; + +/** + * Allows an application to customize the Feign {@link FormattingConversionService}. + * + * @author Matt Benson + */ +public interface FeignFormatterRegistrar extends FormatterRegistrar { + +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java index 6b9d91ed..c67b3ee5 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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. @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,16 +34,19 @@ import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterP import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMapping; -import static feign.Util.checkState; -import static feign.Util.emptyToNull; -import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; - import feign.Contract; import feign.Feign; import feign.MethodMetadata; +import feign.Param; + +import static feign.Util.checkState; +import static feign.Util.emptyToNull; +import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; /** * @author Spencer Gibb @@ -58,14 +62,25 @@ public class SpringMvcContract extends Contract.BaseContract { private final Map, AnnotatedParameterProcessor> annotatedArgumentProcessors; private final Map processedMethods = new HashMap<>(); + private final ConversionService conversionService; + private final Param.Expander expander; + public SpringMvcContract() { this(Collections. emptyList()); } public SpringMvcContract( List annotatedParameterProcessors) { + this(annotatedParameterProcessors, new DefaultConversionService()); + } + + public SpringMvcContract( + List annotatedParameterProcessors, + ConversionService conversionService) { Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null."); + Assert.notNull(conversionService, + "ConversionService can not be null."); List processors; if (!annotatedParameterProcessors.isEmpty()) { @@ -75,6 +90,8 @@ public class SpringMvcContract extends Contract.BaseContract { processors = getDefaultAnnotatedArgumentsProcessors(); } this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors); + this.conversionService = conversionService; + this.expander = new ConvertingExpander(conversionService); } @Override @@ -148,6 +165,8 @@ public class SpringMvcContract extends Contract.BaseContract { // headers parseHeaders(data, method, methodMapping); + + data.indexToExpander(new LinkedHashMap()); } private void checkAtMostOne(Method method, Object[] values, String fieldName) { @@ -184,6 +203,11 @@ public class SpringMvcContract extends Contract.BaseContract { processParameterAnnotation); } } + if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null + && this.conversionService.canConvert( + method.getParameterTypes()[paramIndex], String.class)) { + data.indexToExpander().put(paramIndex, expander); + } return isHttpAnnotation; } @@ -293,4 +317,18 @@ public class SpringMvcContract extends Contract.BaseContract { } } + public static class ConvertingExpander implements Param.Expander { + + private final ConversionService conversionService; + + public ConvertingExpander(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public String expand(Object value) { + return conversionService.convert(value, String.class); + } + + } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTests.java index edf0fd00..bf91e535 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTests.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2016 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.netflix.feign.support; import java.lang.reflect.InvocationTargetException; diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java index 1902d76a..5d464563 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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,9 +18,11 @@ package org.springframework.cloud.netflix.feign.valid; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -34,6 +36,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.WebIntegrationTest; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar; import org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient; import org.springframework.cloud.netflix.feign.support.FallbackCommand; import org.springframework.cloud.netflix.ribbon.RibbonClient; @@ -41,6 +44,8 @@ import org.springframework.cloud.netflix.ribbon.RibbonClients; import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.format.Formatter; +import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -110,6 +115,23 @@ public class FeignClientTests { @Autowired HystrixClient hystrixClient; + protected enum Arg { + A, B; + + @Override + public String toString() { + return name().toLowerCase(Locale.ENGLISH); + } + } + + protected static class OtherArg { + public final String value; + + public OtherArg(String value) { + this.value = value; + } + } + @FeignClient(value = "localapp", configuration = TestClientConfig.class) protected interface TestClient { @RequestMapping(method = RequestMethod.GET, value = "/hello") @@ -147,6 +169,12 @@ public class FeignClientTests { produces = "application/vnd.io.spring.cloud.test.v1+json", value = "/complex") String moreComplexContentType(String body); + + @RequestMapping(method = RequestMethod.GET, value = "/tostring") + String getToString(@RequestParam("arg") Arg arg); + + @RequestMapping(method = RequestMethod.GET, value = "/tostring2") + String getToString(@RequestParam("arg") OtherArg arg); } public static class TestClientConfig { @@ -242,6 +270,28 @@ public class FeignClientTests { }) protected static class Application { + @Bean + FeignFormatterRegistrar feignFormatterRegistrar() { + return new FeignFormatterRegistrar() { + + @Override + public void registerFormatters(FormatterRegistry registry) { + registry.addFormatter(new Formatter() { + + @Override + public String print(OtherArg object, Locale locale) { + return object.value; + } + + @Override + public OtherArg parse(String text, Locale locale) + throws ParseException { + return new OtherArg(text); + } + }); + } + }; + } @RequestMapping(method = RequestMethod.GET, value = "/hello") public Hello getHello() { @@ -304,6 +354,16 @@ public class FeignClientTests { return "{\"value\":\"OK\"}"; } + @RequestMapping(method = RequestMethod.GET, value = "/tostring") + String getToString(@RequestParam("arg") Arg arg) { + return arg.toString(); + } + + @RequestMapping(method = RequestMethod.GET, value = "/tostring2") + String getToString(@RequestParam("arg") OtherArg arg) { + return arg.value; + } + public static void main(String[] args) { new SpringApplicationBuilder(Application.class).properties( "spring.application.name=feignclienttest", @@ -432,6 +492,14 @@ public class FeignClientTests { assertNull("response body was not null", response.getBody()); } + @Test + public void testConvertingExpander() { + assertEquals(Arg.A.toString(), testClient.getToString(Arg.A)); + assertEquals(Arg.B.toString(), testClient.getToString(Arg.B)); + + assertEquals("foo", testClient.getToString(new OtherArg("foo"))); + } + @Test public void testHystrixFallbackWorks() { Hello hello = hystrixClient.fail();