Commit 3f34ccc8 by Matt Benson Committed by Dave Syer

Add Feign expanders based on Spring conversion service

Automatically registers Spring conversion-based Feign Expanders when available Fixes gh-841
parent 05305568
...@@ -33,12 +33,15 @@ import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterP ...@@ -33,12 +33,15 @@ 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 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.checkState;
import static feign.Util.emptyToNull; import static feign.Util.emptyToNull;
...@@ -58,14 +61,24 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -58,14 +61,24 @@ 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;
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 +88,8 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -75,6 +88,8 @@ public class SpringMvcContract extends Contract.BaseContract {
processors = getDefaultAnnotatedArgumentsProcessors(); processors = getDefaultAnnotatedArgumentsProcessors();
} }
this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors); this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
this.conversionService = conversionService;
ConvertingExpander.CONVERSION_SERVICE.set(conversionService);
} }
@Override @Override
...@@ -183,6 +198,11 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -183,6 +198,11 @@ public class SpringMvcContract extends Contract.BaseContract {
processParameterAnnotation); processParameterAnnotation);
} }
} }
if (isHttpAnnotation && data.indexToExpanderClass().get(paramIndex) == null
&& this.conversionService.canConvert(
method.getParameterTypes()[paramIndex], String.class)) {
data.indexToExpanderClass().put(paramIndex, ConvertingExpander.class);
}
return isHttpAnnotation; return isHttpAnnotation;
} }
...@@ -292,4 +312,23 @@ public class SpringMvcContract extends Contract.BaseContract { ...@@ -292,4 +312,23 @@ public class SpringMvcContract extends Contract.BaseContract {
} }
} }
public static class ConvertingExpander implements Param.Expander {
static final ThreadLocal<ConversionService> CONVERSION_SERVICE = new ThreadLocal<ConversionService>() {
protected ConversionService initialValue() {
return new DefaultConversionService();
}
};
private final ConversionService conversionService;
public ConvertingExpander() {
this.conversionService = CONVERSION_SERVICE.get();
}
@Override
public String expand(Object value) {
return conversionService.convert(value, String.class);
}
}
} }
...@@ -29,6 +29,7 @@ import java.lang.reflect.Proxy; ...@@ -29,6 +29,7 @@ import java.lang.reflect.Proxy;
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;
...@@ -105,6 +106,15 @@ public class FeignClientTests { ...@@ -105,6 +106,15 @@ public class FeignClientTests {
@Autowired @Autowired
HystrixClient hystrixClient; HystrixClient hystrixClient;
protected enum Arg {
A, B;
@Override
public String toString() {
return name().toLowerCase(Locale.ENGLISH);
}
}
@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")
...@@ -130,6 +140,9 @@ public class FeignClientTests { ...@@ -130,6 +140,9 @@ public class FeignClientTests {
@RequestMapping(method = RequestMethod.HEAD, value = "/head") @RequestMapping(method = RequestMethod.HEAD, value = "/head")
ResponseEntity head(); ResponseEntity head();
@RequestMapping(method = RequestMethod.GET, value = "/tostring")
String getToString(@RequestParam("arg") Arg arg);
} }
public static class TestClientConfig { public static class TestClientConfig {
...@@ -271,6 +284,10 @@ public class FeignClientTests { ...@@ -271,6 +284,10 @@ public class FeignClientTests {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body((String)null); return ResponseEntity.status(HttpStatus.NOT_FOUND).body((String)null);
} }
@RequestMapping(method = RequestMethod.GET, value = "/tostring")
String getToString(@RequestParam("arg") Arg arg) {
return arg.toString();
}
public static void main(String[] args) { public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).properties( new SpringApplicationBuilder(Application.class).properties(
...@@ -376,6 +393,12 @@ public class FeignClientTests { ...@@ -376,6 +393,12 @@ public class FeignClientTests {
} }
@Test @Test
public void testConvertingExpander() {
assertEquals(Arg.A.toString(), testClient.getToString(Arg.A));
assertEquals(Arg.B.toString(), testClient.getToString(Arg.B));
}
@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