Commit 05305568 by Matt Benson Committed by Dave Syer

Parameter name discovery using compiler parameters

Spring MVC Feign Contract did not support parameter name fallback for the #value() attributes of known parameter annotations. Makes this feature available when the interface has been compiled with the Java 8 -parameters compiler arg Fixes gh-835
parent c3766720
......@@ -199,4 +199,25 @@
<optional>true</optional>
</dependency>
</dependencies>
<profiles>
<profile>
<id>java8plus</id>
<activation>
<jdk>[1.8,2.0)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
......@@ -30,11 +30,14 @@ import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.PathVariableParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestHeaderParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterProcessor;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import feign.Contract;
import feign.Feign;
import feign.MethodMetadata;
import static feign.Util.checkState;
......@@ -50,7 +53,10 @@ public class SpringMvcContract extends Contract.BaseContract {
private static final String CONTENT_TYPE = "Content-Type";
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
private final Map<String, Method> processedMethods = new HashMap<>();
public SpringMvcContract() {
this(Collections.<AnnotatedParameterProcessor> emptyList());
......@@ -73,6 +79,7 @@ public class SpringMvcContract extends Contract.BaseContract {
@Override
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
processedMethods.put(Feign.configKey(targetType, method), method);
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class);
......@@ -162,12 +169,18 @@ public class SpringMvcContract extends Contract.BaseContract {
AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(
data, paramIndex);
Method method = processedMethods.get(data.configKey());
for (Annotation parameterAnnotation : annotations) {
AnnotatedParameterProcessor processor = this.annotatedArgumentProcessors
.get(parameterAnnotation.annotationType());
if (processor != null) {
Annotation processParameterAnnotation;
// synthesize, handling @AliasFor, while falling back to parameter name on
// missing String #value():
processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(
parameterAnnotation, method, paramIndex);
isHttpAnnotation |= processor.processArgument(context,
AnnotationUtils.synthesizeAnnotation(parameterAnnotation, null));
processParameterAnnotation);
}
}
return isHttpAnnotation;
......@@ -227,6 +240,23 @@ public class SpringMvcContract extends Contract.BaseContract {
return annotatedArgumentResolvers;
}
private Annotation synthesizeWithMethodParameterNameAsFallbackValue(
Annotation parameterAnnotation, Method method, int parameterIndex) {
Map<String, Object> annotationAttributes = AnnotationUtils
.getAnnotationAttributes(parameterAnnotation);
Object defaultValue = AnnotationUtils.getDefaultValue(parameterAnnotation);
if (defaultValue instanceof String
&& defaultValue.equals(annotationAttributes.get(AnnotationUtils.VALUE))) {
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
if (parameterNames != null && parameterNames.length > parameterIndex) {
annotationAttributes.put(AnnotationUtils.VALUE,
parameterNames[parameterIndex]);
}
}
return AnnotationUtils.synthesizeAnnotation(annotationAttributes,
parameterAnnotation.annotationType(), null);
}
private class SimpleAnnotatedParameterContext
implements AnnotatedParameterProcessor.AnnotatedParameterContext {
......
package org.springframework.cloud.netflix.feign.support;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
......@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeTrue;
import feign.MethodMetadata;
import lombok.AllArgsConstructor;
......@@ -27,6 +30,18 @@ import lombok.ToString;
* @author chadjaros
*/
public class SpringMvcContractTests {
private static final Class<?> EXECUTABLE_TYPE;
static {
Class<?> executableType;
try {
executableType = Class.forName("java.lang.reflect.Executable");
}
catch (ClassNotFoundException ex) {
executableType = null;
}
EXECUTABLE_TYPE = executableType;
}
private SpringMvcContract contract;
......@@ -167,6 +182,56 @@ public class SpringMvcContractTests {
data.template().headers().get("Accept").iterator().next());
}
@Test
public void testProcessAnnotations_Fallback() throws Exception {
Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTestFallback",
String.class, String.class, Integer.class);
assumeTrue(hasJava8ParameterNames(method));
MethodMetadata data = this.contract
.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertEquals("/advanced/testfallback/{id}", data.template().url());
assertEquals("PUT", data.template().method());
assertEquals(MediaType.APPLICATION_JSON_VALUE,
data.template().headers().get("Accept").iterator().next());
assertEquals("Authorization", data.indexToName().get(0).iterator().next());
assertEquals("id", data.indexToName().get(1).iterator().next());
assertEquals("amount", data.indexToName().get(2).iterator().next());
assertEquals("{Authorization}",
data.template().headers().get("Authorization").iterator().next());
assertEquals("{amount}",
data.template().queries().get("amount").iterator().next());
}
/**
* For abstract (e.g. interface) methods, only Java 8 Parameter names (compiler arg
* -parameters) can supply parameter names; bytecode-based strategies use local
* variable declarations, of which there are none for abstract methods.
* @param m
* @return whether a parameter name was found
* @throws IllegalArgumentException if method has no parameters
*/
private static boolean hasJava8ParameterNames(Method m) {
org.springframework.util.Assert.isTrue(m.getParameterTypes().length > 0,
"method has no parameters");
if (EXECUTABLE_TYPE != null) {
Method getParameters = ReflectionUtils.findMethod(EXECUTABLE_TYPE, "getParameters");
try {
Object[] parameters = (Object[]) getParameters.invoke(m);
Method isNamePresent = ReflectionUtils.findMethod(parameters[0].getClass(), "isNamePresent");
return Boolean.TRUE.equals(isNamePresent.invoke(parameters[0]));
}
catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException ex) {
}
}
return false;
}
public interface TestTemplate_Simple {
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<TestObject> getTest(@PathVariable("id") String id);
......@@ -191,6 +256,11 @@ public class SpringMvcContractTests {
ResponseEntity<TestObject> getTest2(@RequestHeader(name = "Authorization") String auth,
@RequestParam(name = "amount") Integer amount);
@ExceptionHandler
@RequestMapping(path = "/testfallback/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<TestObject> getTestFallback(@RequestHeader String Authorization,
@PathVariable String id, @RequestParam Integer amount);
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
TestObject getTest();
}
......
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