Commit 074b2e6e by Spencer Gibb

Merge pull request #841 from mbenson/feignExpanders

* feignExpanders: Where a given String conversion is supported, register Feign Param.Expanders backed by a Feign-specific FormattingConversionService.
parents b8f11e79 a71e13ef
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; ...@@ -33,6 +33,9 @@ import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope; 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; import com.netflix.hystrix.HystrixCommand;
...@@ -56,6 +59,9 @@ public class FeignClientsConfiguration { ...@@ -56,6 +59,9 @@ public class FeignClientsConfiguration {
@Autowired(required = false) @Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>(); private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Decoder feignDecoder() { public Decoder feignDecoder() {
...@@ -70,8 +76,17 @@ public class FeignClientsConfiguration { ...@@ -70,8 +76,17 @@ public class FeignClientsConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Contract feignContract() { public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors); return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
@Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
} }
@Configuration @Configuration
......
/*
* 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 {
}
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -23,6 +23,7 @@ import java.util.Arrays; ...@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -33,16 +34,19 @@ import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterP ...@@ -33,16 +34,19 @@ import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterP
import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils; 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.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping; 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.Contract;
import feign.Feign; import feign.Feign;
import feign.MethodMetadata; 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 * @author Spencer Gibb
...@@ -58,14 +62,25 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -58,14 +62,25 @@ public class SpringMvcContract extends Contract.BaseContract {
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors; private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
private final Map<String, Method> processedMethods = new HashMap<>(); private final Map<String, Method> processedMethods = new HashMap<>();
private final ConversionService conversionService;
private final Param.Expander expander;
public SpringMvcContract() { public SpringMvcContract() {
this(Collections.<AnnotatedParameterProcessor> emptyList()); this(Collections.<AnnotatedParameterProcessor> emptyList());
} }
public SpringMvcContract( public SpringMvcContract(
List<AnnotatedParameterProcessor> annotatedParameterProcessors) { List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
this(annotatedParameterProcessors, new DefaultConversionService());
}
public SpringMvcContract(
List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService) {
Assert.notNull(annotatedParameterProcessors, Assert.notNull(annotatedParameterProcessors,
"Parameter processors can not be null."); "Parameter processors can not be null.");
Assert.notNull(conversionService,
"ConversionService can not be null.");
List<AnnotatedParameterProcessor> processors; List<AnnotatedParameterProcessor> processors;
if (!annotatedParameterProcessors.isEmpty()) { if (!annotatedParameterProcessors.isEmpty()) {
...@@ -75,6 +90,8 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -75,6 +90,8 @@ public class SpringMvcContract extends Contract.BaseContract {
processors = getDefaultAnnotatedArgumentsProcessors(); processors = getDefaultAnnotatedArgumentsProcessors();
} }
this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors); this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
this.conversionService = conversionService;
this.expander = new ConvertingExpander(conversionService);
} }
@Override @Override
...@@ -148,6 +165,8 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -148,6 +165,8 @@ public class SpringMvcContract extends Contract.BaseContract {
// headers // headers
parseHeaders(data, method, methodMapping); parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
} }
private void checkAtMostOne(Method method, Object[] values, String fieldName) { private void checkAtMostOne(Method method, Object[] values, String fieldName) {
...@@ -184,6 +203,11 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -184,6 +203,11 @@ public class SpringMvcContract extends Contract.BaseContract {
processParameterAnnotation); processParameterAnnotation);
} }
} }
if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null
&& this.conversionService.canConvert(
method.getParameterTypes()[paramIndex], String.class)) {
data.indexToExpander().put(paramIndex, expander);
}
return isHttpAnnotation; return isHttpAnnotation;
} }
...@@ -293,4 +317,18 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -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);
}
}
} }
/*
* 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; package org.springframework.cloud.netflix.feign.support;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; ...@@ -18,9 +18,11 @@ package org.springframework.cloud.netflix.feign.valid;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
...@@ -34,6 +36,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration; ...@@ -34,6 +36,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest; import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.FeignClient; 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.ribbon.LoadBalancerFeignClient;
import org.springframework.cloud.netflix.feign.support.FallbackCommand; import org.springframework.cloud.netflix.feign.support.FallbackCommand;
import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.RibbonClient;
...@@ -41,6 +44,8 @@ import org.springframework.cloud.netflix.ribbon.RibbonClients; ...@@ -41,6 +44,8 @@ import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.HttpEntity;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
...@@ -110,6 +115,23 @@ public class FeignClientTests { ...@@ -110,6 +115,23 @@ public class FeignClientTests {
@Autowired @Autowired
HystrixClient hystrixClient; 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) @FeignClient(value = "localapp", configuration = TestClientConfig.class)
protected interface TestClient { protected interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
...@@ -147,6 +169,12 @@ public class FeignClientTests { ...@@ -147,6 +169,12 @@ public class FeignClientTests {
produces = "application/vnd.io.spring.cloud.test.v1+json", produces = "application/vnd.io.spring.cloud.test.v1+json",
value = "/complex") value = "/complex")
String moreComplexContentType(String body); 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 { public static class TestClientConfig {
...@@ -242,6 +270,28 @@ public class FeignClientTests { ...@@ -242,6 +270,28 @@ public class FeignClientTests {
}) })
protected static class Application { protected static class Application {
@Bean
FeignFormatterRegistrar feignFormatterRegistrar() {
return new FeignFormatterRegistrar() {
@Override
public void registerFormatters(FormatterRegistry registry) {
registry.addFormatter(new Formatter<OtherArg>() {
@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") @RequestMapping(method = RequestMethod.GET, value = "/hello")
public Hello getHello() { public Hello getHello() {
...@@ -304,6 +354,16 @@ public class FeignClientTests { ...@@ -304,6 +354,16 @@ public class FeignClientTests {
return "{\"value\":\"OK\"}"; 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) { public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).properties( new SpringApplicationBuilder(Application.class).properties(
"spring.application.name=feignclienttest", "spring.application.name=feignclienttest",
...@@ -433,6 +493,14 @@ public class FeignClientTests { ...@@ -433,6 +493,14 @@ public class FeignClientTests {
} }
@Test @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() { public void testHystrixFallbackWorks() {
Hello hello = hystrixClient.fail(); Hello hello = hystrixClient.fail();
assertNotNull("hello was null", hello); assertNotNull("hello was null", hello);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment