Where a given String conversion is supported, register Feign Param.Expanders
backed by a Feign-specific FormattingConversionService.
This commit is contained in:
committed by
Spencer Gibb
parent
b8f11e79c6
commit
a71e13ef07
@@ -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<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<FeignFormatterRegistrar> 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
|
||||
private final Map<String, Method> processedMethods = new HashMap<>();
|
||||
|
||||
private final ConversionService conversionService;
|
||||
private final Param.Expander expander;
|
||||
|
||||
public SpringMvcContract() {
|
||||
this(Collections.<AnnotatedParameterProcessor> emptyList());
|
||||
}
|
||||
|
||||
public SpringMvcContract(
|
||||
List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
|
||||
this(annotatedParameterProcessors, new DefaultConversionService());
|
||||
}
|
||||
|
||||
public SpringMvcContract(
|
||||
List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
ConversionService conversionService) {
|
||||
Assert.notNull(annotatedParameterProcessors,
|
||||
"Parameter processors can not be null.");
|
||||
Assert.notNull(conversionService,
|
||||
"ConversionService can not be null.");
|
||||
|
||||
List<AnnotatedParameterProcessor> 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<Integer, Param.Expander>());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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")
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user