From c2749f1631194ad6bb9313b0f06e7eee3c5da5ea Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Fri, 13 Sep 2013 17:08:27 -0400 Subject: [PATCH] Add Feign.Builder (#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. --- CHANGES.md | 1 + README.md | 24 ++- core/src/main/java/feign/Feign.java | 169 +++++++++++++++++- .../src/test/java/feign/FeignBuilderTest.java | 126 +++++++++++++ 4 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 core/src/test/java/feign/FeignBuilderTest.java diff --git a/CHANGES.md b/CHANGES.md index fc377164..023a13f7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * Remove pattern decoders in favor of SaxDecoder. * Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders. * Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. +* Added Feign.Builder to simplify client customizations without using Dagger. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/README.md b/README.md index 7a67d04e..57d0c475 100644 --- a/README.md +++ b/README.md @@ -40,23 +40,33 @@ public static void main(String... args) { Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own. +### Customization + +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: + +```java +interface Bank { + @RequestLine("POST /account/{id}") + Account getAccountInfo(@Named("id") String id); +} +... +Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com"); +``` + +For further flexibility, you can use Dagger modules directly. See the `Dagger` section for more details. + ### Request Interceptors When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`. For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header. ``` -@Module(library = true) static class ForwardedForInterceptor implements RequestInterceptor { - @Provides(type = SET) RequestInterceptor provideThis() { - return this; - } - @Override public void apply(RequestTemplate template) { template.header("X-Forwarded-For", "origin.host.com"); } } ... -GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new ForwardedForInterceptor()); +Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com"); ``` ### Multiple Interfaces @@ -65,7 +75,7 @@ Feign can produce multiple api interfaces. These are defined as `Target` (de For example, the following pattern might decorate each request with the current url and auth token from the identity service. ```java -CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget(user, apiKey)); +CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget(user, apiKey)); ``` 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! diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index 6bbf4715..820ea260 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -16,6 +16,7 @@ package feign; +import dagger.Module; import dagger.ObjectGraph; import dagger.Provides; import feign.Logger.NoOpLogger; @@ -25,12 +26,15 @@ import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; +import javax.inject.Inject; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * Feign's purpose is to ease development against http apis that feign @@ -48,6 +52,10 @@ public abstract class Feign { */ public abstract T newInstance(Target target); + public static Builder builder() { + return new Builder(); + } + public static T create(Class apiType, String url, Object... modules) { return create(new HardCodedTarget(apiType, url), modules); } @@ -78,7 +86,7 @@ public abstract class Feign { } @SuppressWarnings("rawtypes") - @dagger.Module(complete = false, injects = Feign.class, library = true) + @dagger.Module(complete = false, injects = {Feign.class, Builder.class}, library = true) public static class Defaults { @Provides Logger.Level logLevel() { @@ -168,4 +176,163 @@ public abstract class Feign { modulesForGraph.add(module); return modulesForGraph; } + + public static class Builder { + private final Set requestInterceptors = new LinkedHashSet(); + @Inject Logger.Level logLevel; + @Inject Contract contract; + @Inject Client client; + @Inject Retryer retryer; + @Inject Logger logger; + @Inject Encoder encoder; + @Inject Decoder decoder; + @Inject ErrorDecoder errorDecoder; + @Inject Options options; + + Builder() { + ObjectGraph.create(new Defaults()).inject(this); + } + + public Builder logLevel(Logger.Level logLevel) { + this.logLevel = logLevel; + return this; + } + + public Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + public Builder client(Client client) { + this.client = client; + return this; + } + + public Builder retryer(Retryer retryer) { + this.retryer = retryer; + return this; + } + + public Builder logger(Logger logger) { + this.logger = logger; + return this; + } + + public Builder encoder(Encoder encoder) { + this.encoder = encoder; + return this; + } + + public Builder decoder(Decoder decoder) { + this.decoder = decoder; + return this; + } + + public Builder errorDecoder(ErrorDecoder errorDecoder) { + this.errorDecoder = errorDecoder; + return this; + } + + public Builder options(Options options) { + this.options = options; + return this; + } + + /** + * Adds a single request interceptor to the builder. + */ + public Builder requestInterceptor(RequestInterceptor requestInterceptor) { + this.requestInterceptors.add(requestInterceptor); + return this; + } + + /** + * Sets the full set of request interceptors for the builder, overwriting any previous interceptors. + */ + public Builder requestInterceptors(Iterable requestInterceptors) { + this.requestInterceptors.clear(); + for (RequestInterceptor requestInterceptor : requestInterceptors) { + this.requestInterceptors.add(requestInterceptor); + } + return this; + } + + public T target(Class apiType, String url) { + return target(new HardCodedTarget(apiType, url)); + } + + public T target(Target target) { + BuilderModule module = new BuilderModule(this); + return create(module).newInstance(target); + } + } + + @Module(library = true, overrides = true, addsTo = Defaults.class) + static class BuilderModule { + private final Logger.Level logLevel; + private final Contract contract; + private final Client client; + private final Retryer retryer; + private final Logger logger; + private final Encoder encoder; + private final Decoder decoder; + private final ErrorDecoder errorDecoder; + private final Options options; + private final Set requestInterceptors; + + BuilderModule(Builder builder) { + this.logLevel = builder.logLevel; + this.contract = builder.contract; + this.client = builder.client; + this.retryer = builder.retryer; + this.logger = builder.logger; + this.encoder = builder.encoder; + this.decoder = builder.decoder; + this.errorDecoder = builder.errorDecoder; + this.options = builder.options; + this.requestInterceptors = builder.requestInterceptors; + } + + @Provides Logger.Level logLevel() { + return logLevel; + } + + @Provides Contract contract() { + return contract; + } + + @Provides Client client() { + return client; + } + + @Provides Retryer retryer() { + return retryer; + } + + @Provides Logger logger() { + return logger; + } + + @Provides + Encoder encoder() { + return encoder; + } + + @Provides + Decoder decoder() { + return decoder; + } + + @Provides ErrorDecoder errorDecoder() { + return errorDecoder; + } + + @Provides Options options() { + return options; + } + + @Provides(type = Provides.Type.SET_VALUES) Set requestInterceptors() { + return requestInterceptors; + } + } } diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java new file mode 100644 index 00000000..097e80ac --- /dev/null +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign; + +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.RecordedRequest; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import org.testng.annotations.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.assertEquals; + +public class FeignBuilderTest { + interface TestInterface { + @RequestLine("POST /") Response codecPost(String data); + + @RequestLine("POST /") void encodedPost(List data); + + @RequestLine("POST /") String decodedPost(); + } + + @Test public void testDefaults() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + try { + TestInterface api = Feign.builder().target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals(Util.toString(response.body().asReader()), "response data"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + assertEquals(server.takeRequest().getUtf8Body(), "request data"); + } + } + + @Test public void testOverrideEncoder() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + Encoder encoder = new Encoder() { + @Override + public void encode(Object object, RequestTemplate template) throws EncodeException { + template.body(object.toString()); + } + }; + try { + TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); + api.encodedPost(Arrays.asList("This", "is", "my", "request")); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + assertEquals(server.takeRequest().getUtf8Body(), "[This, is, my, request]"); + } + } + + @Test public void testOverrideDecoder() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("success!")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + Decoder decoder = new Decoder() { + @Override + public Object decode(Response response, Type type) { + return "fail"; + } + }; + + try { + TestInterface api = Feign.builder().decoder(decoder).target(TestInterface.class, url); + assertEquals(api.decodedPost(), "fail"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + } + } + + @Test public void testProvideRequestInterceptors() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + RequestInterceptor requestInterceptor = new RequestInterceptor() { + @Override + public void apply(RequestTemplate template) { + template.header("Content-Type", "text/plain"); + } + }; + try { + TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals(Util.toString(response.body().asReader()), "response data"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + RecordedRequest request = server.takeRequest(); + assertEquals(request.getUtf8Body(), "request data"); + assertEquals(request.getHeader("Content-Type"), "text/plain"); + } + } +}