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
This commit is contained in:
@@ -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 {
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user