Skip to content

Commit a9e631a

Browse files
authored
Add extra information when failing to parse contract (#1176)
* Add extra information when failing to parse contract * Add warnings to other error messages * Move check if processor are valid before if block
1 parent 68f7984 commit a9e631a

File tree

4 files changed

+195
-105
lines changed

4 files changed

+195
-105
lines changed

core/src/main/java/feign/Contract.java

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
5252
"Only single-level inheritance supported: %s",
5353
targetType.getSimpleName());
5454
}
55-
Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
56-
for (Method method : targetType.getMethods()) {
55+
final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
56+
for (final Method method : targetType.getMethods()) {
5757
if (method.getDeclaringClass() == Object.class ||
5858
(method.getModifiers() & Modifier.STATIC) != 0 ||
5959
Util.isDefault(method)) {
6060
continue;
6161
}
62-
MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
62+
final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
6363
checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
6464
metadata.configKey());
6565
result.put(metadata.configKey(), metadata);
@@ -79,7 +79,7 @@ public MethodMetadata parseAndValidateMetadata(Method method) {
7979
* Called indirectly by {@link #parseAndValidateMetadata(Class)}.
8080
*/
8181
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
82-
MethodMetadata data = new MethodMetadata();
82+
final MethodMetadata data = new MethodMetadata();
8383
data.targetType(targetType);
8484
data.method(method);
8585
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
@@ -91,20 +91,20 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
9191
processAnnotationOnClass(data, targetType);
9292

9393

94-
for (Annotation methodAnnotation : method.getAnnotations()) {
94+
for (final Annotation methodAnnotation : method.getAnnotations()) {
9595
processAnnotationOnMethod(data, methodAnnotation, method);
9696
}
9797
if (data.isIgnored()) {
9898
return data;
9999
}
100100
checkState(data.template().method() != null,
101-
"Method %s not annotated with HTTP method type (ex. GET, POST)",
102-
data.configKey());
103-
Class<?>[] parameterTypes = method.getParameterTypes();
104-
Type[] genericParameterTypes = method.getGenericParameterTypes();
101+
"Method %s not annotated with HTTP method type (ex. GET, POST)%s",
102+
data.configKey(), data.warnings());
103+
final Class<?>[] parameterTypes = method.getParameterTypes();
104+
final Type[] genericParameterTypes = method.getGenericParameterTypes();
105105

106-
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
107-
int count = parameterAnnotations.length;
106+
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
107+
final int count = parameterAnnotations.length;
108108
for (int i = 0; i < count; i++) {
109109
boolean isHttpAnnotation = false;
110110
if (parameterAnnotations[i] != null) {
@@ -120,11 +120,12 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
120120
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
121121
if (data.isAlreadyProcessed(i)) {
122122
checkState(data.formParams().isEmpty() || data.bodyIndex() == null,
123-
"Body parameters cannot be used with form parameters.");
123+
"Body parameters cannot be used with form parameters.%s", data.warnings());
124124
} else {
125125
checkState(data.formParams().isEmpty(),
126-
"Body parameters cannot be used with form parameters.");
127-
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
126+
"Body parameters cannot be used with form parameters.%s", data.warnings());
127+
checkState(data.bodyIndex() == null,
128+
"Method has too many Body parameters: %s%s", method, data.warnings());
128129
data.bodyIndex(i);
129130
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
130131
}
@@ -156,17 +157,17 @@ private static void checkMapKeys(String name, Type genericType) {
156157

157158
// assume our type parameterized
158159
if (ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
159-
Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
160+
final Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
160161
keyClass = (Class<?>) parameterTypes[0];
161162
} else if (genericType instanceof Class<?>) {
162163
// raw class, type parameters cannot be inferred directly, but we can scan any extended
163164
// interfaces looking for any explict types
164-
Type[] interfaces = ((Class) genericType).getGenericInterfaces();
165+
final Type[] interfaces = ((Class) genericType).getGenericInterfaces();
165166
if (interfaces != null) {
166-
for (Type extended : interfaces) {
167+
for (final Type extended : interfaces) {
167168
if (ParameterizedType.class.isAssignableFrom(extended.getClass())) {
168169
// use the first extended interface we find.
169-
Type[] parameterTypes = ((ParameterizedType) extended).getActualTypeArguments();
170+
final Type[] parameterTypes = ((ParameterizedType) extended).getActualTypeArguments();
170171
keyClass = (Class<?>) parameterTypes[0];
171172
break;
172173
}
@@ -214,7 +215,7 @@ protected abstract boolean processAnnotationsOnParameter(MethodMetadata data,
214215
* links a parameter name to its index in the method signature.
215216
*/
216217
protected void nameParam(MethodMetadata data, String name, int i) {
217-
Collection<String> names =
218+
final Collection<String> names =
218219
data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList<String>();
219220
names.add(name);
220221
data.indexToName().put(i, names);
@@ -227,20 +228,20 @@ class Default extends DeclarativeContract {
227228

228229
public Default() {
229230
super.registerClassAnnotation(Headers.class, (header, data) -> {
230-
String[] headersOnType = header.value();
231+
final String[] headersOnType = header.value();
231232
checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
232233
data.configKey());
233-
Map<String, Collection<String>> headers = toMap(headersOnType);
234+
final Map<String, Collection<String>> headers = toMap(headersOnType);
234235
headers.putAll(data.template().headers());
235236
data.template().headers(null); // to clear
236237
data.template().headers(headers);
237238
});
238239
super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
239-
String requestLine = ann.value();
240+
final String requestLine = ann.value();
240241
checkState(emptyToNull(requestLine) != null,
241242
"RequestLine annotation was empty on method %s.", data.configKey());
242243

243-
Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
244+
final Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
244245
if (!requestLineMatcher.find()) {
245246
throw new IllegalStateException(String.format(
246247
"RequestLine annotation didn't start with an HTTP verb on method %s",
@@ -254,7 +255,7 @@ public Default() {
254255
.collectionFormat(ann.collectionFormat());
255256
});
256257
super.registerMethodAnnotation(Body.class, (ann, data) -> {
257-
String body = ann.value();
258+
final String body = ann.value();
258259
checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
259260
data.configKey());
260261
if (body.indexOf('{') == -1) {
@@ -264,17 +265,17 @@ public Default() {
264265
}
265266
});
266267
super.registerMethodAnnotation(Headers.class, (header, data) -> {
267-
String[] headersOnMethod = header.value();
268+
final String[] headersOnMethod = header.value();
268269
checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
269270
data.configKey());
270271
data.template().headers(toMap(headersOnMethod));
271272
});
272273
super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
273-
String name = paramAnnotation.value();
274+
final String name = paramAnnotation.value();
274275
checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
275276
paramIndex);
276277
nameParam(data, name, paramIndex);
277-
Class<? extends Param.Expander> expander = paramAnnotation.expander();
278+
final Class<? extends Param.Expander> expander = paramAnnotation.expander();
278279
if (expander != Param.ToStringExpander.class) {
279280
data.indexToExpanderClass().put(paramIndex, expander);
280281
}
@@ -296,11 +297,11 @@ public Default() {
296297
}
297298

298299
private static Map<String, Collection<String>> toMap(String[] input) {
299-
Map<String, Collection<String>> result =
300+
final Map<String, Collection<String>> result =
300301
new LinkedHashMap<String, Collection<String>>(input.length);
301-
for (String header : input) {
302-
int colon = header.indexOf(':');
303-
String name = header.substring(0, colon);
302+
for (final String header : input) {
303+
final int colon = header.indexOf(':');
304+
final String name = header.substring(0, colon);
304305
if (!result.containsKey(name)) {
305306
result.put(name, new ArrayList<String>(1));
306307
}

core/src/main/java/feign/DeclarativeContract.java

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2012-2019 The Feign Authors
2+
* Copyright 2012-2020 The Feign Authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
55
* in compliance with the License. You may obtain a copy of the License at
@@ -15,8 +15,10 @@
1515

1616
import java.lang.annotation.Annotation;
1717
import java.lang.reflect.Method;
18+
import java.lang.reflect.Parameter;
1819
import java.util.*;
1920
import java.util.function.Predicate;
21+
import java.util.stream.Collectors;
2022
import feign.Contract.BaseContract;
2123

2224
/**
@@ -25,9 +27,9 @@
2527
*/
2628
public abstract class DeclarativeContract extends BaseContract {
2729

28-
private List<GuardedAnnotationProcessor> classAnnotationProcessors = new ArrayList<>();
29-
private List<GuardedAnnotationProcessor> methodAnnotationProcessors = new ArrayList<>();
30-
Map<Class<Annotation>, DeclarativeContract.ParameterAnnotationProcessor<Annotation>> parameterAnnotationProcessors =
30+
private final List<GuardedAnnotationProcessor> classAnnotationProcessors = new ArrayList<>();
31+
private final List<GuardedAnnotationProcessor> methodAnnotationProcessors = new ArrayList<>();
32+
private final Map<Class<Annotation>, DeclarativeContract.ParameterAnnotationProcessor<Annotation>> parameterAnnotationProcessors =
3133
new HashMap<>();
3234

3335
@Override
@@ -45,10 +47,33 @@ public final List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType)
4547
*/
4648
@Override
4749
protected final void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) {
48-
Arrays.stream(targetType.getAnnotations())
49-
.forEach(annotation -> classAnnotationProcessors.stream()
50-
.filter(processor -> processor.test(annotation))
51-
.forEach(processor -> processor.process(annotation, data)));
50+
final List<GuardedAnnotationProcessor> processors = Arrays.stream(targetType.getAnnotations())
51+
.flatMap(annotation -> classAnnotationProcessors.stream()
52+
.filter(processor -> processor.test(annotation)))
53+
.collect(Collectors.toList());
54+
55+
if (!processors.isEmpty()) {
56+
Arrays.stream(targetType.getAnnotations())
57+
.forEach(annotation -> processors.stream()
58+
.filter(processor -> processor.test(annotation))
59+
.forEach(processor -> processor.process(annotation, data)));
60+
} else {
61+
if (targetType.getAnnotations().length == 0) {
62+
data.addWarning(String.format(
63+
"Class %s has no annotations, it may affect contract %s",
64+
targetType.getSimpleName(),
65+
getClass().getSimpleName()));
66+
} else {
67+
data.addWarning(String.format(
68+
"Class %s has annotations %s that are not used by contract %s",
69+
targetType.getSimpleName(),
70+
Arrays.stream(targetType.getAnnotations())
71+
.map(annotation -> annotation.annotationType()
72+
.getSimpleName())
73+
.collect(Collectors.toList()),
74+
getClass().getSimpleName()));
75+
}
76+
}
5277
}
5378

5479
/**
@@ -60,9 +85,20 @@ protected final void processAnnotationOnClass(MethodMetadata data, Class<?> targ
6085
protected final void processAnnotationOnMethod(MethodMetadata data,
6186
Annotation annotation,
6287
Method method) {
63-
methodAnnotationProcessors.stream()
88+
List<GuardedAnnotationProcessor> processors = methodAnnotationProcessors.stream()
6489
.filter(processor -> processor.test(annotation))
65-
.forEach(processor -> processor.process(annotation, data));
90+
.collect(Collectors.toList());
91+
92+
if (!processors.isEmpty()) {
93+
processors.forEach(processor -> processor.process(annotation, data));
94+
} else {
95+
data.addWarning(String.format(
96+
"Method %s has an annotation %s that is not used by contract %s",
97+
method.getName(),
98+
annotation.annotationType()
99+
.getSimpleName(),
100+
getClass().getSimpleName()));
101+
}
66102
}
67103

68104

@@ -78,12 +114,37 @@ protected final void processAnnotationOnMethod(MethodMetadata data,
78114
protected final boolean processAnnotationsOnParameter(MethodMetadata data,
79115
Annotation[] annotations,
80116
int paramIndex) {
81-
Arrays.stream(annotations)
117+
List<Annotation> matchingAnnotations = Arrays.stream(annotations)
82118
.filter(
83119
annotation -> parameterAnnotationProcessors.containsKey(annotation.annotationType()))
84-
.forEach(annotation -> parameterAnnotationProcessors
85-
.getOrDefault(annotation.annotationType(), ParameterAnnotationProcessor.DO_NOTHING)
86-
.process(annotation, data, paramIndex));
120+
.collect(Collectors.toList());
121+
122+
if (!matchingAnnotations.isEmpty()) {
123+
matchingAnnotations.forEach(annotation -> parameterAnnotationProcessors
124+
.getOrDefault(annotation.annotationType(), ParameterAnnotationProcessor.DO_NOTHING)
125+
.process(annotation, data, paramIndex));
126+
127+
} else {
128+
final Parameter parameter = data.method().getParameters()[paramIndex];
129+
String parameterName = parameter.isNamePresent()
130+
? parameter.getName()
131+
: parameter.getType().getSimpleName();
132+
if (annotations.length == 0) {
133+
data.addWarning(String.format(
134+
"Parameter %s has no annotations, it may affect contract %s",
135+
parameterName,
136+
getClass().getSimpleName()));
137+
} else {
138+
data.addWarning(String.format(
139+
"Parameter %s has annotations %s that are not used by contract %s",
140+
parameterName,
141+
Arrays.stream(annotations)
142+
.map(annotation -> annotation.annotationType()
143+
.getSimpleName())
144+
.collect(Collectors.toList()),
145+
getClass().getSimpleName()));
146+
}
147+
}
87148
return false;
88149
}
89150

@@ -177,8 +238,8 @@ public interface ParameterAnnotationProcessor<E extends Annotation> {
177238
private class GuardedAnnotationProcessor
178239
implements Predicate<Annotation>, DeclarativeContract.AnnotationProcessor<Annotation> {
179240

180-
private Predicate<Annotation> predicate;
181-
private DeclarativeContract.AnnotationProcessor<Annotation> processor;
241+
private final Predicate<Annotation> predicate;
242+
private final DeclarativeContract.AnnotationProcessor<Annotation> processor;
182243

183244
@SuppressWarnings({"rawtypes", "unchecked"})
184245
private GuardedAnnotationProcessor(Predicate predicate,

core/src/main/java/feign/MethodMetadata.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2012-2019 The Feign Authors
2+
* Copyright 2012-2020 The Feign Authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
55
* in compliance with the License. You may obtain a copy of the License at
@@ -17,6 +17,7 @@
1717
import java.lang.reflect.Method;
1818
import java.lang.reflect.Type;
1919
import java.util.*;
20+
import java.util.stream.Collectors;
2021
import feign.Param.Expander;
2122

2223
public final class MethodMetadata implements Serializable {
@@ -30,18 +31,19 @@ public final class MethodMetadata implements Serializable {
3031
private Integer queryMapIndex;
3132
private boolean queryMapEncoded;
3233
private transient Type bodyType;
33-
private RequestTemplate template = new RequestTemplate();
34-
private List<String> formParams = new ArrayList<String>();
35-
private Map<Integer, Collection<String>> indexToName =
34+
private final RequestTemplate template = new RequestTemplate();
35+
private final List<String> formParams = new ArrayList<String>();
36+
private final Map<Integer, Collection<String>> indexToName =
3637
new LinkedHashMap<Integer, Collection<String>>();
37-
private Map<Integer, Class<? extends Expander>> indexToExpanderClass =
38+
private final Map<Integer, Class<? extends Expander>> indexToExpanderClass =
3839
new LinkedHashMap<Integer, Class<? extends Expander>>();
39-
private Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
40+
private final Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
4041
private transient Map<Integer, Expander> indexToExpander;
4142
private BitSet parameterToIgnore = new BitSet();
4243
private boolean ignored;
4344
private transient Class<?> targetType;
4445
private transient Method method;
46+
private transient final List<String> warnings = new ArrayList<>();
4547

4648
MethodMetadata() {
4749
template.methodMetadata(this);
@@ -240,4 +242,13 @@ public Method method() {
240242
return method;
241243
}
242244

245+
public void addWarning(String warning) {
246+
warnings.add(warning);
247+
}
248+
249+
public String warnings() {
250+
return warnings.stream()
251+
.collect(Collectors.joining("\n- ", "\nWarnings:\n- ", ""));
252+
}
253+
243254
}

0 commit comments

Comments
 (0)