Skip to content

Commit c2749f1

Browse files
committed
Add Feign.Builder (OpenFeign#34)
For those who do not use Dagger, or do not wish to, this provides an alternate method of defining dependencies. This includes logging config, decoders, etc. It still uses Dagger under the scenes, but doesn't require the user to deal with the module system.
1 parent 99760f7 commit c2749f1

4 files changed

Lines changed: 312 additions & 8 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Remove pattern decoders in favor of SaxDecoder.
55
* Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders.
66
* Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively.
7+
* Added Feign.Builder to simplify client customizations without using Dagger.
78

89
### Version 4.4.1
910
* Fix NullPointerException on calling equals and hashCode.

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,33 @@ public static void main(String... args) {
4040

4141
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
4242

43+
### Customization
44+
45+
Feign has several aspects that can be customized. For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components. For example:
46+
47+
```java
48+
interface Bank {
49+
@RequestLine("POST /account/{id}")
50+
Account getAccountInfo(@Named("id") String id);
51+
}
52+
...
53+
Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com");
54+
```
55+
56+
For further flexibility, you can use Dagger modules directly. See the `Dagger` section for more details.
57+
4358
### Request Interceptors
4459
When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
4560
For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header.
4661

4762
```
48-
@Module(library = true)
4963
static class ForwardedForInterceptor implements RequestInterceptor {
50-
@Provides(type = SET) RequestInterceptor provideThis() {
51-
return this;
52-
}
53-
5464
@Override public void apply(RequestTemplate template) {
5565
template.header("X-Forwarded-For", "origin.host.com");
5666
}
5767
}
5868
...
59-
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new ForwardedForInterceptor());
69+
Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com");
6070
```
6171

6272
### Multiple Interfaces
@@ -65,7 +75,7 @@ Feign can produce multiple api interfaces. These are defined as `Target<T>` (de
6575
For example, the following pattern might decorate each request with the current url and auth token from the identity service.
6676

6777
```java
68-
CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget<CloudDNS>(user, apiKey));
78+
CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget<CloudDNS>(user, apiKey));
6979
```
7080

7181
You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing!

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

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package feign;
1717

1818

19+
import dagger.Module;
1920
import dagger.ObjectGraph;
2021
import dagger.Provides;
2122
import feign.Logger.NoOpLogger;
@@ -25,12 +26,15 @@
2526
import feign.codec.Encoder;
2627
import feign.codec.ErrorDecoder;
2728

29+
import javax.inject.Inject;
2830
import javax.net.ssl.HostnameVerifier;
2931
import javax.net.ssl.HttpsURLConnection;
3032
import javax.net.ssl.SSLSocketFactory;
3133
import java.lang.reflect.Method;
3234
import java.util.ArrayList;
35+
import java.util.LinkedHashSet;
3336
import java.util.List;
37+
import java.util.Set;
3438

3539
/**
3640
* Feign's purpose is to ease development against http apis that feign
@@ -48,6 +52,10 @@ public abstract class Feign {
4852
*/
4953
public abstract <T> T newInstance(Target<T> target);
5054

55+
public static Builder builder() {
56+
return new Builder();
57+
}
58+
5159
public static <T> T create(Class<T> apiType, String url, Object... modules) {
5260
return create(new HardCodedTarget<T>(apiType, url), modules);
5361
}
@@ -78,7 +86,7 @@ public static ObjectGraph createObjectGraph(Object... modules) {
7886
}
7987

8088
@SuppressWarnings("rawtypes")
81-
@dagger.Module(complete = false, injects = Feign.class, library = true)
89+
@dagger.Module(complete = false, injects = {Feign.class, Builder.class}, library = true)
8290
public static class Defaults {
8391

8492
@Provides Logger.Level logLevel() {
@@ -168,4 +176,163 @@ private static List<Object> modulesForGraph(Object... modules) {
168176
modulesForGraph.add(module);
169177
return modulesForGraph;
170178
}
179+
180+
public static class Builder {
181+
private final Set<RequestInterceptor> requestInterceptors = new LinkedHashSet<RequestInterceptor>();
182+
@Inject Logger.Level logLevel;
183+
@Inject Contract contract;
184+
@Inject Client client;
185+
@Inject Retryer retryer;
186+
@Inject Logger logger;
187+
@Inject Encoder encoder;
188+
@Inject Decoder decoder;
189+
@Inject ErrorDecoder errorDecoder;
190+
@Inject Options options;
191+
192+
Builder() {
193+
ObjectGraph.create(new Defaults()).inject(this);
194+
}
195+
196+
public Builder logLevel(Logger.Level logLevel) {
197+
this.logLevel = logLevel;
198+
return this;
199+
}
200+
201+
public Builder contract(Contract contract) {
202+
this.contract = contract;
203+
return this;
204+
}
205+
206+
public Builder client(Client client) {
207+
this.client = client;
208+
return this;
209+
}
210+
211+
public Builder retryer(Retryer retryer) {
212+
this.retryer = retryer;
213+
return this;
214+
}
215+
216+
public Builder logger(Logger logger) {
217+
this.logger = logger;
218+
return this;
219+
}
220+
221+
public Builder encoder(Encoder encoder) {
222+
this.encoder = encoder;
223+
return this;
224+
}
225+
226+
public Builder decoder(Decoder decoder) {
227+
this.decoder = decoder;
228+
return this;
229+
}
230+
231+
public Builder errorDecoder(ErrorDecoder errorDecoder) {
232+
this.errorDecoder = errorDecoder;
233+
return this;
234+
}
235+
236+
public Builder options(Options options) {
237+
this.options = options;
238+
return this;
239+
}
240+
241+
/**
242+
* Adds a single request interceptor to the builder.
243+
*/
244+
public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
245+
this.requestInterceptors.add(requestInterceptor);
246+
return this;
247+
}
248+
249+
/**
250+
* Sets the full set of request interceptors for the builder, overwriting any previous interceptors.
251+
*/
252+
public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
253+
this.requestInterceptors.clear();
254+
for (RequestInterceptor requestInterceptor : requestInterceptors) {
255+
this.requestInterceptors.add(requestInterceptor);
256+
}
257+
return this;
258+
}
259+
260+
public <T> T target(Class<T> apiType, String url) {
261+
return target(new HardCodedTarget<T>(apiType, url));
262+
}
263+
264+
public <T> T target(Target<T> target) {
265+
BuilderModule module = new BuilderModule(this);
266+
return create(module).newInstance(target);
267+
}
268+
}
269+
270+
@Module(library = true, overrides = true, addsTo = Defaults.class)
271+
static class BuilderModule {
272+
private final Logger.Level logLevel;
273+
private final Contract contract;
274+
private final Client client;
275+
private final Retryer retryer;
276+
private final Logger logger;
277+
private final Encoder encoder;
278+
private final Decoder decoder;
279+
private final ErrorDecoder errorDecoder;
280+
private final Options options;
281+
private final Set<RequestInterceptor> requestInterceptors;
282+
283+
BuilderModule(Builder builder) {
284+
this.logLevel = builder.logLevel;
285+
this.contract = builder.contract;
286+
this.client = builder.client;
287+
this.retryer = builder.retryer;
288+
this.logger = builder.logger;
289+
this.encoder = builder.encoder;
290+
this.decoder = builder.decoder;
291+
this.errorDecoder = builder.errorDecoder;
292+
this.options = builder.options;
293+
this.requestInterceptors = builder.requestInterceptors;
294+
}
295+
296+
@Provides Logger.Level logLevel() {
297+
return logLevel;
298+
}
299+
300+
@Provides Contract contract() {
301+
return contract;
302+
}
303+
304+
@Provides Client client() {
305+
return client;
306+
}
307+
308+
@Provides Retryer retryer() {
309+
return retryer;
310+
}
311+
312+
@Provides Logger logger() {
313+
return logger;
314+
}
315+
316+
@Provides
317+
Encoder encoder() {
318+
return encoder;
319+
}
320+
321+
@Provides
322+
Decoder decoder() {
323+
return decoder;
324+
}
325+
326+
@Provides ErrorDecoder errorDecoder() {
327+
return errorDecoder;
328+
}
329+
330+
@Provides Options options() {
331+
return options;
332+
}
333+
334+
@Provides(type = Provides.Type.SET_VALUES) Set<RequestInterceptor> requestInterceptors() {
335+
return requestInterceptors;
336+
}
337+
}
171338
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2013 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.mockwebserver.MockResponse;
19+
import com.google.mockwebserver.MockWebServer;
20+
import com.google.mockwebserver.RecordedRequest;
21+
import feign.codec.Decoder;
22+
import feign.codec.EncodeException;
23+
import feign.codec.Encoder;
24+
import org.testng.annotations.Test;
25+
26+
import java.lang.reflect.Type;
27+
import java.util.Arrays;
28+
import java.util.List;
29+
30+
import static org.testng.Assert.assertEquals;
31+
32+
public class FeignBuilderTest {
33+
interface TestInterface {
34+
@RequestLine("POST /") Response codecPost(String data);
35+
36+
@RequestLine("POST /") void encodedPost(List<String> data);
37+
38+
@RequestLine("POST /") String decodedPost();
39+
}
40+
41+
@Test public void testDefaults() throws Exception {
42+
MockWebServer server = new MockWebServer();
43+
server.enqueue(new MockResponse().setBody("response data"));
44+
server.play();
45+
46+
String url = "http://localhost:" + server.getPort();
47+
try {
48+
TestInterface api = Feign.builder().target(TestInterface.class, url);
49+
Response response = api.codecPost("request data");
50+
assertEquals(Util.toString(response.body().asReader()), "response data");
51+
} finally {
52+
server.shutdown();
53+
assertEquals(server.getRequestCount(), 1);
54+
assertEquals(server.takeRequest().getUtf8Body(), "request data");
55+
}
56+
}
57+
58+
@Test public void testOverrideEncoder() throws Exception {
59+
MockWebServer server = new MockWebServer();
60+
server.enqueue(new MockResponse().setBody("response data"));
61+
server.play();
62+
63+
String url = "http://localhost:" + server.getPort();
64+
Encoder encoder = new Encoder() {
65+
@Override
66+
public void encode(Object object, RequestTemplate template) throws EncodeException {
67+
template.body(object.toString());
68+
}
69+
};
70+
try {
71+
TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url);
72+
api.encodedPost(Arrays.asList("This", "is", "my", "request"));
73+
} finally {
74+
server.shutdown();
75+
assertEquals(server.getRequestCount(), 1);
76+
assertEquals(server.takeRequest().getUtf8Body(), "[This, is, my, request]");
77+
}
78+
}
79+
80+
@Test public void testOverrideDecoder() throws Exception {
81+
MockWebServer server = new MockWebServer();
82+
server.enqueue(new MockResponse().setBody("success!"));
83+
server.play();
84+
85+
String url = "http://localhost:" + server.getPort();
86+
Decoder decoder = new Decoder() {
87+
@Override
88+
public Object decode(Response response, Type type) {
89+
return "fail";
90+
}
91+
};
92+
93+
try {
94+
TestInterface api = Feign.builder().decoder(decoder).target(TestInterface.class, url);
95+
assertEquals(api.decodedPost(), "fail");
96+
} finally {
97+
server.shutdown();
98+
assertEquals(server.getRequestCount(), 1);
99+
}
100+
}
101+
102+
@Test public void testProvideRequestInterceptors() throws Exception {
103+
MockWebServer server = new MockWebServer();
104+
server.enqueue(new MockResponse().setBody("response data"));
105+
server.play();
106+
107+
String url = "http://localhost:" + server.getPort();
108+
RequestInterceptor requestInterceptor = new RequestInterceptor() {
109+
@Override
110+
public void apply(RequestTemplate template) {
111+
template.header("Content-Type", "text/plain");
112+
}
113+
};
114+
try {
115+
TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url);
116+
Response response = api.codecPost("request data");
117+
assertEquals(Util.toString(response.body().asReader()), "response data");
118+
} finally {
119+
server.shutdown();
120+
assertEquals(server.getRequestCount(), 1);
121+
RecordedRequest request = server.takeRequest();
122+
assertEquals(request.getUtf8Body(), "request data");
123+
assertEquals(request.getHeader("Content-Type"), "text/plain");
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)