Browse Source

Merge pull request #49 from Netflix/request-interceptor

add RequestInterceptor
pull/50/merge
Adrian Cole 12 years ago
parent
commit
d6fa73159c
  1. 3
      CHANGES.md
  2. 19
      README.md
  3. 35
      core/src/main/java/feign/MethodHandler.java
  4. 11
      core/src/main/java/feign/ReflectiveFeign.java
  5. 70
      core/src/main/java/feign/RequestInterceptor.java
  6. 15
      core/src/main/java/feign/RequestTemplate.java
  7. 2
      core/src/main/java/feign/Target.java
  8. 59
      core/src/test/java/feign/FeignTest.java

3
CHANGES.md

@ -1,3 +1,6 @@ @@ -1,3 +1,6 @@
### Version 4.3
* Add ability to configure zero or more RequestInterceptors.
### Version 4.2/3.3
* Document and enforce JAX-RS annotation processing from server POV
* Skip query template parameters when corresponding java arg is null

19
README.md

@ -37,6 +37,25 @@ public static void main(String... args) { @@ -37,6 +37,25 @@ 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.
### 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());
```
### Observable Methods
If specified as the last return type of a method `Observable<T>` will invoke a new http request for each call to `subscribe()`. This is the async equivalent to an `Iterable`.
Here's how one looks:

35
core/src/main/java/feign/MethodHandler.java

@ -27,6 +27,7 @@ import javax.inject.Named; @@ -27,6 +27,7 @@ import javax.inject.Named;
import javax.inject.Provider;
import java.io.IOException;
import java.io.Reader;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -44,29 +45,31 @@ interface MethodHandler { @@ -44,29 +45,31 @@ interface MethodHandler {
private final Client client;
private final Lazy<Executor> httpExecutor;
private final Provider<Retryer> retryer;
private final Set<RequestInterceptor> requestInterceptors;
private final Logger logger;
private final Provider<Logger.Level> logLevel;
@Inject Factory(Client client, @Named("http") Lazy<Executor> httpExecutor, Provider<Retryer> retryer, Logger logger,
Provider<Logger.Level> logLevel) {
@Inject Factory(Client client, @Named("http") Lazy<Executor> httpExecutor, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger, Provider<Logger.Level> logLevel) {
this.client = checkNotNull(client, "client");
this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor");
this.retryer = checkNotNull(retryer, "retryer");
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
this.logger = checkNotNull(logger, "logger");
this.logLevel = checkNotNull(logLevel, "logLevel");
}
public MethodHandler create(Target<?> target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
Options options, Decoder.TextStream<?> decoder, ErrorDecoder errorDecoder) {
return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, options,
decoder, errorDecoder);
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md,
buildTemplateFromArgs, options, decoder, errorDecoder);
}
public MethodHandler create(Target<?> target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
Options options, IncrementalDecoder.TextStream<?> incrementalDecoder,
ErrorDecoder errorDecoder) {
ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, logger, logLevel, md,
buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor);
ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, requestInterceptors, logger,
logLevel, md, buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor);
return new ObservableMethodHandler(observerHandler);
}
}
@ -106,12 +109,14 @@ interface MethodHandler { @@ -106,12 +109,14 @@ interface MethodHandler {
private final Lazy<Executor> httpExecutor;
private final IncrementalDecoder.TextStream<?> incrementalDecoder;
private ObserverHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
private ObserverHandler(Target<?> target, Client client, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options,
IncrementalDecoder.TextStream<?> incrementalDecoder, ErrorDecoder errorDecoder,
Lazy<Executor> httpExecutor) {
super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options,
errorDecoder);
this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor for %s", target);
this.incrementalDecoder = checkNotNull(incrementalDecoder, "incrementalDecoder for %s", target);
}
@ -185,11 +190,13 @@ interface MethodHandler { @@ -185,11 +190,13 @@ interface MethodHandler {
static class SynchronousMethodHandler extends BaseMethodHandler {
private final Decoder.TextStream<?> decoder;
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options,
Decoder.TextStream<?> decoder, ErrorDecoder errorDecoder) {
super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options,
errorDecoder);
this.decoder = checkNotNull(decoder, "decoder for %s", target);
}
@ -215,18 +222,21 @@ interface MethodHandler { @@ -215,18 +222,21 @@ interface MethodHandler {
protected final Target<?> target;
protected final Client client;
protected final Provider<Retryer> retryer;
protected final Set<RequestInterceptor> requestInterceptors;
protected final Logger logger;
protected final Provider<Logger.Level> logLevel;
protected final BuildTemplateFromArgs buildTemplateFromArgs;
protected final Options options;
protected final ErrorDecoder errorDecoder;
private BaseMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
private BaseMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options, ErrorDecoder errorDecoder) {
this.target = checkNotNull(target, "target");
this.client = checkNotNull(client, "client for %s", target);
this.retryer = checkNotNull(retryer, "retryer for %s", target);
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
this.logger = checkNotNull(logger, "logger for %s", target);
this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
this.metadata = checkNotNull(metadata, "metadata for %s", target);
@ -294,6 +304,9 @@ interface MethodHandler { @@ -294,6 +304,9 @@ interface MethodHandler {
}
protected Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(new RequestTemplate(template));
}

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

@ -35,6 +35,7 @@ import java.lang.reflect.Type; @@ -35,6 +35,7 @@ import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -105,19 +106,17 @@ public class ReflectiveFeign extends Feign { @@ -105,19 +106,17 @@ public class ReflectiveFeign extends Feign {
}
}
@dagger.Module(complete = false, injects = Feign.class, library = true)
@dagger.Module(complete = false, injects = {Feign.class, MethodHandler.Factory.class}, library = true)
public static class Module {
@Provides(type = Provides.Type.SET_VALUES) Set<RequestInterceptor> noRequestInterceptors() {
return new LinkedHashSet<RequestInterceptor>();
}
@Provides Feign provideFeign(ReflectiveFeign in) {
return in;
}
}
private static IllegalStateException noConfig(String configKey, Class<?> type) {
return new IllegalStateException(format("no configuration for %s present for %s!", configKey,
type.getSimpleName()));
}
static final class ParseHandlersByName {
private final Contract contract;
private final Options options;

70
core/src/main/java/feign/RequestInterceptor.java

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
/*
* 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;
/**
* Zero or more {@code RequestInterceptors} may be configured for purposes
* such as adding headers to all requests. No guarantees are give with regards
* to the order that interceptors are applied. Once interceptors are applied,
* {@link Target#apply(RequestTemplate)} is called to create the immutable http
* request sent via {@link Client#execute(Request, feign.Request.Options)}.
* <br>
* <br>
* For example:
* <br>
* <pre>
* public void apply(RequestTemplate input) {
* input.replaceHeader(&quot;X-Auth&quot;, currentToken);
* }
* </pre>
* <br>
* <br><b>Configuration</b><br>
* <br>
* {@code RequestInterceptors} are configured via Dagger
* {@link dagger.Provides.Type#SET set} or
* {@link dagger.Provides.Type#SET_VALUES set values}
* {@link dagger.Provides provider} methods.
* <br>
* <br>
* For example:
* <br>
* <pre>
* {@literal @}Provides(Type = SET) RequestInterceptor addTimestamp(TimestampInterceptor in) {
* return in;
* }
* </pre>
* <br>
* <br><b>Implementation notes</b><br>
* <br>
* Do not add parameters, such as {@code /path/{foo}/bar }
* in your implementation of {@link #apply(RequestTemplate)}.
* <br>
* Interceptors are applied after the template's parameters are
* {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure
* that you can implement signatures are interceptors.
* <br>
* <br><br><b>Relationship to Retrofit 1.x</b><br>
* <br>
* This class is similar to {@code RequestInterceptor.intercept()},
* except that the implementation can read, remove, or otherwise mutate any
* part of the request template.
*/
public interface RequestInterceptor {
/**
* Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}

15
core/src/main/java/feign/RequestTemplate.java

@ -73,19 +73,8 @@ public final class RequestTemplate implements Serializable { @@ -73,19 +73,8 @@ public final class RequestTemplate implements Serializable {
}
/**
* Targets a template to this target, adding the {@link #url() base url} and
* any authentication headers.
* <br>
* <br>
* For example:
* <br>
* <pre>
* public Request apply(RequestTemplate input) {
* input.insert(0, url());
* input.replaceHeader(&quot;X-Auth&quot;, currentToken);
* return input.asRequest();
* }
* </pre>
* Resolves any templated variables in the requests path, query, or headers
* against the supplied unencoded arguments.
* <br>
* <br><br><b>relationship to JAXRS 2.0</b><br>
* <br>

2
core/src/main/java/feign/Target.java

@ -40,7 +40,7 @@ public interface Target<T> { @@ -40,7 +40,7 @@ public interface Target<T> {
/**
* Targets a template to this target, adding the {@link #url() base url} and
* any authentication headers.
* any target-specific headers or query parameters.
* <br>
* <br>
* For example:

59
core/src/test/java/feign/FeignTest.java

@ -18,6 +18,7 @@ package feign; @@ -18,6 +18,7 @@ package feign;
import com.google.common.base.Joiner;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
import com.google.mockwebserver.SocketPolicy;
import dagger.Lazy;
import dagger.Module;
@ -308,6 +309,64 @@ public class FeignTest { @@ -308,6 +309,64 @@ public class FeignTest {
}
}
@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");
}
}
@Test
public void singleInterceptor() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new ForwardedForInterceptor());
api.post();
assertEquals(server.takeRequest().getHeader("X-Forwarded-For"), "origin.host.com");
} finally {
server.shutdown();
}
}
@Module(library = true)
static class UserAgentInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("User-Agent", "Feign");
}
}
@Test
public void multipleInterceptor() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor());
api.post();
RecordedRequest request = server.takeRequest();
assertEquals(request.getHeader("X-Forwarded-For"), "origin.host.com");
assertEquals(request.getHeader("User-Agent"), "Feign");
} finally {
server.shutdown();
}
}
@Test public void toKeyMethodFormatsAsExpected() throws Exception {
assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()");
assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class,

Loading…
Cancel
Save