Skip to content

Commit 0aac921

Browse files
Adrian ColeAdrian Cole
authored andcommitted
Adds base api support via single-inheritance interfaces
Before this change, apis that follow patterns across a service could only be modeled by copy/paste/find/replace. Especially with a large count, this is monotonous and error prone. This change introduces support for base apis via single-inheritance interfaces. Users ensure their target interface bind any type variables and as a result have little effort to create boilerplate apis. Ex. ```java @headers("Accept: application/json") interface BaseApi<V> { @RequestLine("GET /api/{key}") V get(@Param("key") String); @RequestLine("GET /api") List<V> list(); @headers("Content-Type: application/json") @RequestLine("PUT /api/{key}") void put(@Param("key") String, V value); } interface FooApi extends BaseApi<Foo> { } interface BarApi extends BaseApi<Bar> { } ``` closes OpenFeign#133
1 parent cd0c108 commit 0aac921

13 files changed

Lines changed: 462 additions & 224 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 8.6
2+
* Adds base api support via single-inheritance interfaces
3+
14
### Version 7.5/8.5
25
* Added possibility to leave slash encoded in path parameters
36

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,50 @@ client.json("denominator", "secret"); // {"user_name": "denominator", "password"
238238

239239
### Advanced usage
240240

241+
#### Base Apis
242+
In many cases, apis for a service follow the same conventions. Feign supports this pattern via single-inheritance interfaces.
243+
244+
Consider the example:
245+
```java
246+
interface BaseAPI {
247+
@RequestLine("GET /health")
248+
String health();
249+
250+
@RequestLine("GET /all")
251+
List<Entity> all();
252+
}
253+
```
254+
255+
You can define and target a specific api, inheriting the base methods.
256+
```java
257+
interface CustomAPI extends BaseAPI {
258+
@RequestLine("GET /custom")
259+
String custom();
260+
}
261+
```
262+
263+
In many cases, resource representations are also consistent. For this reason, type parameters are supported on the base api interface.
264+
265+
```java
266+
@Headers("Accept: application/json")
267+
interface BaseApi<V> {
268+
269+
@RequestLine("GET /api/{key}")
270+
V get(@Param("key") String);
271+
272+
@RequestLine("GET /api")
273+
List<V> list();
274+
275+
@Headers("Content-Type: application/json")
276+
@RequestLine("PUT /api/{key}")
277+
void put(@Param("key") String, V value);
278+
}
279+
280+
interface FooApi extends BaseApi<Foo> { }
281+
282+
interface BarApi extends BaseApi<Bar> { }
283+
```
284+
241285
#### Logging
242286
You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that:
243287
```java

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

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,53 @@ public interface Contract {
3434

3535
/**
3636
* Called to parse the methods in the class that are linked to HTTP requests.
37+
*
38+
* @param targetType {@link feign.Target#type() type} of the Feign interface.
3739
*/
38-
List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring);
40+
// TODO: break this and correct spelling at some point
41+
List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
3942

4043
abstract class BaseContract implements Contract {
4144

4245
@Override
43-
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
44-
List<MethodMetadata> metadata = new ArrayList<MethodMetadata>();
45-
for (Method method : declaring.getDeclaredMethods()) {
46+
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
47+
checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
48+
targetType.getSimpleName());
49+
checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
50+
targetType.getSimpleName());
51+
if (targetType.getInterfaces().length == 1) {
52+
checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
53+
"Only single-level inheritance supported: %s",
54+
targetType.getSimpleName());
55+
}
56+
Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
57+
for (Method method : targetType.getMethods()) {
4658
if (method.getDeclaringClass() == Object.class) {
4759
continue;
4860
}
49-
metadata.add(parseAndValidatateMetadata(method));
61+
MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
62+
checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
63+
metadata.configKey());
64+
result.put(metadata.configKey(), metadata);
5065
}
51-
return metadata;
66+
return new ArrayList<MethodMetadata>(result.values());
5267
}
5368

5469
/**
55-
* Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
70+
* @deprecated use {@link #parseAndValidateMetadata(Class, Method)} instead.
5671
*/
72+
@Deprecated
5773
public MethodMetadata parseAndValidatateMetadata(Method method) {
74+
return parseAndValidateMetadata(method.getDeclaringClass(), method);
75+
}
76+
77+
/**
78+
* Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
79+
*/
80+
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
5881
MethodMetadata data = new MethodMetadata();
59-
data.returnType(method.getGenericReturnType());
60-
data.configKey(Feign.configKey(method));
82+
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
83+
data.configKey(Feign.configKey(targetType, method));
6184

6285
for (Annotation methodAnnotation : method.getAnnotations()) {
6386
processAnnotationOnMethod(data, methodAnnotation, method);
@@ -81,7 +104,7 @@ public MethodMetadata parseAndValidatateMetadata(Method method) {
81104
"Body parameters cannot be used with form parameters.");
82105
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
83106
data.bodyIndex(i);
84-
data.bodyType(method.getGenericParameterTypes()[i]);
107+
data.bodyType(Types.resolve(targetType, targetType, method.getGenericParameterTypes()[i]));
85108
}
86109
}
87110
return data;
@@ -131,18 +154,25 @@ protected void nameParam(MethodMetadata data, String name, int i) {
131154
class Default extends BaseContract {
132155

133156
@Override
134-
public MethodMetadata parseAndValidatateMetadata(Method method) {
135-
MethodMetadata data = super.parseAndValidatateMetadata(method);
136-
if (method.getDeclaringClass().isAnnotationPresent(Headers.class)) {
137-
String[] headersOnType = method.getDeclaringClass().getAnnotation(Headers.class).value();
157+
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
158+
MethodMetadata data = super.parseAndValidateMetadata(targetType, method);
159+
headersFromAnnotation(method.getDeclaringClass(), data);
160+
if (method.getDeclaringClass() != targetType) {
161+
headersFromAnnotation(targetType, data);
162+
}
163+
return data;
164+
}
165+
166+
private void headersFromAnnotation(Class<?> targetType, MethodMetadata data) {
167+
if (targetType.isAnnotationPresent(Headers.class)) {
168+
String[] headersOnType = targetType.getAnnotation(Headers.class).value();
138169
checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
139-
method.getDeclaringClass().getName());
170+
targetType.getName());
140171
Map<String, Collection<String>> headers = toMap(headersOnType);
141172
headers.putAll(data.template().headers());
142173
data.template().headers(null); // to clear
143174
data.template().headers(headers);
144175
}
145-
return data;
146176
}
147177

148178
@Override

core/src/main/java/feign/Feign.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package feign;
1717

1818
import java.lang.reflect.Method;
19+
import java.lang.reflect.Type;
1920
import java.util.ArrayList;
2021
import java.util.List;
2122

@@ -47,20 +48,32 @@ public static Builder builder() {
4748
* Route53#listByNameAndType(String, String)}: would match a method such as {@code
4849
* denominator.route53.Route53#listAt(String, String)} </ul> <br> Note that there is no whitespace
4950
* expected in a key!
51+
*
52+
* @param targetType {@link feign.Target#type() type} of the Feign interface.
53+
* @param method invoked method, present on {@code type} or its super.
5054
*/
51-
public static String configKey(Method method) {
55+
public static String configKey(Class targetType, Method method) {
5256
StringBuilder builder = new StringBuilder();
53-
builder.append(method.getDeclaringClass().getSimpleName());
57+
builder.append(targetType.getSimpleName());
5458
builder.append('#').append(method.getName()).append('(');
55-
for (Class<?> param : method.getParameterTypes()) {
56-
builder.append(param.getSimpleName()).append(',');
59+
for (Type param : method.getGenericParameterTypes()) {
60+
param = Types.resolve(targetType, targetType, param);
61+
builder.append(Types.getRawType(param).getSimpleName()).append(',');
5762
}
5863
if (method.getParameterTypes().length > 0) {
5964
builder.deleteCharAt(builder.length() - 1);
6065
}
6166
return builder.append(')').toString();
6267
}
6368

69+
/**
70+
* @deprecated use {@link #configKey(Class, Method)} instead.
71+
*/
72+
@Deprecated
73+
public static String configKey(Method method) {
74+
return configKey(method.getDeclaringClass(), method);
75+
}
76+
6477
/**
6578
* Returns a new instance of an HTTP API, defined by annotations in the {@link Feign Contract},
6679
* for the specified {@code target}. You should cache this result.

core/src/main/java/feign/Logger.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected static String methodTag(String configKey) {
4040
* Override to log requests and responses using your own implementation. Messages will be http
4141
* request and response text.
4242
*
43-
* @param configKey value of {@link Feign#configKey(java.lang.reflect.Method)}
43+
* @param configKey value of {@link Feign#configKey(Class, java.lang.reflect.Method)}
4444
* @param format {@link java.util.Formatter format string}
4545
* @param args arguments applied to {@code format}
4646
*/

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ public final class MethodMetadata implements Serializable {
3535
private transient Type bodyType;
3636
private RequestTemplate template = new RequestTemplate();
3737
private List<String> formParams = new ArrayList<String>();
38-
private Map<Integer, Collection<String>>
39-
indexToName =
38+
private Map<Integer, Collection<String>> indexToName =
4039
new LinkedHashMap<Integer, Collection<String>>();
4140
private Map<Integer, Class<? extends Expander>> indexToExpanderClass =
4241
new LinkedHashMap<Integer, Class<? extends Expander>>();
@@ -45,7 +44,7 @@ public final class MethodMetadata implements Serializable {
4544
}
4645

4746
/**
48-
* @see Feign#configKey(java.lang.reflect.Method)
47+
* @see Feign#configKey(Class, java.lang.reflect.Method)
4948
*/
5049
public String configKey() {
5150
return configKey;

core/src/main/java/feign/ReflectiveFeign.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ public class ReflectiveFeign extends Feign {
5454
public <T> T newInstance(Target<T> target) {
5555
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
5656
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
57-
for (Method method : target.type().getDeclaredMethods()) {
57+
for (Method method : target.type().getMethods()) {
5858
if (method.getDeclaringClass() == Object.class) {
5959
continue;
6060
}
61-
methodToHandler.put(method, nameToHandler.get(Feign.configKey(method)));
61+
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
6262
}
6363
InvocationHandler handler = factory.create(target, methodToHandler);
6464
return (T) Proxy

core/src/main/java/feign/Util.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@ public static void ensureClosed(Closeable closeable) {
168168
*/
169169
public static Type resolveLastTypeParameter(Type genericContext, Class<?> supertype)
170170
throws IllegalStateException {
171-
Type
172-
resolvedSuperType =
171+
Type resolvedSuperType =
173172
Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype);
174173
checkState(resolvedSuperType instanceof ParameterizedType,
175174
"could not resolve %s into a parameterized type %s",
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2015 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign;
17+
18+
import com.google.gson.reflect.TypeToken;
19+
20+
import com.squareup.okhttp.mockwebserver.MockResponse;
21+
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
22+
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
26+
import java.lang.reflect.Type;
27+
import java.util.List;
28+
29+
import feign.codec.Decoder;
30+
import feign.codec.Encoder;
31+
32+
import static feign.assertj.MockWebServerAssertions.assertThat;
33+
34+
public class BaseApiTest {
35+
36+
@Rule
37+
public final MockWebServerRule server = new MockWebServerRule();
38+
39+
interface BaseApi<K, M> {
40+
41+
@RequestLine("GET /api/{key}")
42+
Entity<K, M> get(@Param("key") K key);
43+
44+
@RequestLine("POST /api")
45+
Entities<K, M> getAll(Keys<K> keys);
46+
}
47+
48+
static class Keys<K> {
49+
50+
List<K> keys;
51+
}
52+
53+
static class Entity<K, M> {
54+
55+
K key;
56+
M model;
57+
}
58+
59+
static class Entities<K, M> {
60+
61+
List<Entity<K, M>> entities;
62+
}
63+
64+
interface MyApi extends BaseApi<String, Long> {
65+
66+
}
67+
68+
@Test
69+
public void resolvesParameterizedResult() throws InterruptedException {
70+
server.enqueue(new MockResponse().setBody("foo"));
71+
72+
String baseUrl = server.getUrl("/default").toString();
73+
74+
Feign.builder()
75+
.decoder(new Decoder() {
76+
@Override
77+
public Object decode(Response response, Type type) {
78+
assertThat(type)
79+
.isEqualTo(new TypeToken<Entity<String, Long>>() {
80+
}.getType());
81+
return null;
82+
}
83+
})
84+
.target(MyApi.class, baseUrl).get("foo");
85+
86+
assertThat(server.takeRequest()).hasPath("/default/api/foo");
87+
}
88+
89+
@Test
90+
public void resolvesBodyParameter() throws InterruptedException {
91+
server.enqueue(new MockResponse().setBody("foo"));
92+
93+
String baseUrl = server.getUrl("/default").toString();
94+
95+
Feign.builder()
96+
.encoder(new Encoder() {
97+
@Override
98+
public void encode(Object object, Type bodyType, RequestTemplate template) {
99+
assertThat(bodyType)
100+
.isEqualTo(new TypeToken<Keys<String>>() {
101+
}.getType());
102+
}
103+
})
104+
.decoder(new Decoder() {
105+
@Override
106+
public Object decode(Response response, Type type) {
107+
assertThat(type)
108+
.isEqualTo(new TypeToken<Entities<String, Long>>() {
109+
}.getType());
110+
return null;
111+
}
112+
})
113+
.target(MyApi.class, baseUrl).getAll(new Keys<String>());
114+
}
115+
}

0 commit comments

Comments
 (0)