Skip to content

Commit 222fc12

Browse files
author
adriancole
committed
initial import
1 parent 54f3dd4 commit 222fc12

35 files changed

+3549
-27
lines changed

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,16 @@ Thumbs.db
3939
# Gradle Files #
4040
################
4141
.gradle
42+
local.properties
4243

4344
# Build output directies
4445
/target
45-
*/target
46-
/build
46+
**/test-output
47+
**/target
48+
**/bin
49+
build
4750
*/build
51+
.m2
4852

4953
# IntelliJ specific files/directories
5054
out

README.md

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,100 @@
1-
feign
2-
=====
1+
# Feign makes writing java http clients easier
2+
Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [jclouds](https://github.com/jclouds/jclouds), and [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
3+
4+
### Why Feign and not X?
5+
6+
You can use tools like Jersey and CXF to write java clients for ReST or SOAP services. You can write your own code on top of http transport libraries like Apache HC. Feign aims to connect your code to http apis with minimal overhead and code. Via customizable decoders and error handling, you should be able to write to any text-based http api.
7+
8+
### How does Feign work?
9+
10+
Feign works by processing annotations into a templatized request. Just before sending it off, arguments are applied to these templates in a straightforward fashion. While this limits Feign to only supporting text-based apis, it dramatically simplified system aspects such as replaying requests. It is also stupid easy to unit test your conversions knowing this.
11+
12+
### Basics
13+
14+
Usage typically looks like this, an adaptation of the [canonical Retrofit sample](https://github.com/square/retrofit/blob/master/retrofit-samples/github-client/src/main/java/com/example/retrofit/GitHubClient.java).
15+
16+
```java
17+
interface GitHub {
18+
@GET @Path("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
19+
}
20+
21+
static class Contributor {
22+
String login;
23+
int contributions;
24+
}
25+
26+
public static void main(String... args) {
27+
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
28+
29+
// Fetch and print a list of the contributors to this library.
30+
List<Contributor> contributors = github.contributors("netflix", "feign");
31+
for (Contributor contributor : contributors) {
32+
System.out.println(contributor.login + " (" + contributor.contributions + ")");
33+
}
34+
}
35+
```
36+
### Decoders
37+
The last argument to `Feign.create` specifies how to decode the responses. You can plug-in your favorite library, such as gson, or use builtin RegEx Pattern decoders. Here's how the Gson module looks.
38+
39+
```java
40+
@Module(overrides = true, library = true)
41+
static class GsonModule {
42+
@Provides @Singleton Map<String, Decoder> decoders() {
43+
return ImmutableMap.of("GitHub", gsonDecoder);
44+
}
45+
46+
final Decoder gsonDecoder = new Decoder() {
47+
Gson gson = new Gson();
48+
49+
@Override public Object decode(String methodKey, Reader reader, TypeToken<?> type) {
50+
return gson.fromJson(reader, type.getType());
51+
}
52+
};
53+
}
54+
```
55+
Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in. If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use.
56+
57+
### Multiple Interfaces
58+
Feign can produce multiple api interfaces. These are defined as `Target<T>` (default `HardCodedTarget<T>`), which allow for dynamic discovery and decoration of requests prior to execution.
59+
60+
For example, the following pattern might decorate each request with the current url and auth token from the identity service.
61+
62+
```java
63+
CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget<CloudDNS>(user, apiKey));
64+
```
65+
66+
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!
67+
### Advanced usage and Dagger
68+
#### Dagger
69+
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.
70+
71+
Almost all configuration of Feign is represented as Map bindings, where the key is either the simple name (ex. `GitHub`) or the method (ex. `GitHub#contributors()`) in javadoc link format. For example, the following routes all decoding to gson:
72+
```java
73+
@Provides @Singleton Map<String, Decoder> decoders() {
74+
return ImmutableMap.of("GitHub", gsonDecoder);
75+
}
76+
```
77+
#### Wire Logging
78+
You can log the http messages going to and from the target by setting up a `Wire`. Here's the easiest way to do that:
79+
```java
80+
@Module(overrides = true)
81+
class Overrides {
82+
@Provides @Singleton Wire provideWire() {
83+
return new Wire.LoggingWire().appendToFile("logs/http-wire.log");
84+
}
85+
}
86+
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides());
87+
```
88+
#### Pattern Decoders
89+
If you have to only grab a single field from a server response, you may find regular expressions less maintenance than writing a type adapter.
90+
91+
Here's how our IAM example grabs only one xml element from a response.
92+
```java
93+
@Module(overrides = true, library = true)
94+
static class IAMModule {
95+
@Provides @Singleton Map<String, Decoder> decoders() {
96+
return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>"));
97+
}
98+
}
99+
```
100+

build.gradle

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,19 @@ apply from: file('gradle/release.gradle')
2323

2424
subprojects {
2525
group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project
26-
27-
dependencies {
28-
compile 'javax.ws.rs:jsr311-api:1.1.1'
29-
compile 'com.sun.jersey:jersey-core:1.11'
30-
testCompile 'org.testng:testng:6.1.1'
31-
testCompile 'org.mockito:mockito-core:1.8.5'
32-
}
3326
}
3427

35-
project(':template-client') {
28+
project(':feign-core') {
3629
apply plugin: 'java'
37-
dependencies {
38-
compile 'org.slf4j:slf4j-api:1.6.3'
39-
compile 'com.sun.jersey:jersey-client:1.11'
40-
}
41-
}
4230

43-
project(':template-server') {
44-
apply plugin: 'war'
45-
apply plugin: 'jetty'
4631
dependencies {
47-
compile 'com.sun.jersey:jersey-server:1.11'
48-
compile 'com.sun.jersey:jersey-servlet:1.11'
49-
compile project(':template-client')
50-
}
32+
compile 'com.google.guava:guava:14.0.1'
33+
compile 'com.squareup.dagger:dagger:1.0.1'
34+
compile 'javax.ws.rs:jsr311-api:1.1.1'
35+
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
36+
testCompile 'com.google.code.gson:gson:2.2.4'
37+
testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2'
38+
testCompile 'org.testng:testng:6.8.1'
39+
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
40+
}
5141
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package feign;
2+
3+
import com.google.common.collect.ImmutableListMultimap;
4+
import com.google.common.io.ByteSink;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.io.InputStreamReader;
9+
import java.io.OutputStream;
10+
import java.io.Reader;
11+
import java.net.HttpURLConnection;
12+
import java.net.URL;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Map.Entry;
16+
17+
import javax.inject.Inject;
18+
import javax.net.ssl.HttpsURLConnection;
19+
import javax.net.ssl.SSLSocketFactory;
20+
21+
import dagger.Lazy;
22+
import feign.Request.Options;
23+
24+
import static com.google.common.base.Charsets.UTF_8;
25+
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
26+
27+
/**
28+
* Submits HTTP {@link Request requests}. Implementations are expected to be
29+
* thread-safe.
30+
*/
31+
public interface Client {
32+
/**
33+
* Executes a request against its {@link Request#url() url} and returns a
34+
* response.
35+
*
36+
* @param request safe to replay.
37+
* @param options options to apply to this request.
38+
* @return connected response, {@link Response.Body} is absent or unread.
39+
* @throws IOException on a network error connecting to {@link Request#url()}.
40+
*/
41+
Response execute(Request request, Options options) throws IOException;
42+
43+
public static class Default implements Client {
44+
private final Lazy<SSLSocketFactory> sslContextFactory;
45+
46+
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory) {
47+
this.sslContextFactory = sslContextFactory;
48+
}
49+
50+
@Override public Response execute(Request request, Options options) throws IOException {
51+
HttpURLConnection connection = convertAndSend(request, options);
52+
return convertResponse(connection);
53+
}
54+
55+
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
56+
final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
57+
if (connection instanceof HttpsURLConnection) {
58+
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
59+
sslCon.setSSLSocketFactory(sslContextFactory.get());
60+
}
61+
connection.setConnectTimeout(options.connectTimeoutMillis());
62+
connection.setReadTimeout(options.readTimeoutMillis());
63+
connection.setAllowUserInteraction(false);
64+
connection.setInstanceFollowRedirects(true);
65+
connection.setRequestMethod(request.method());
66+
67+
Integer contentLength = null;
68+
for (Entry<String, String> header : request.headers().entries()) {
69+
if (header.getKey().equals(CONTENT_LENGTH))
70+
contentLength = Integer.valueOf(header.getValue());
71+
connection.addRequestProperty(header.getKey(), header.getValue());
72+
}
73+
74+
if (request.body().isPresent()) {
75+
if (contentLength != null) {
76+
connection.setFixedLengthStreamingMode(contentLength);
77+
} else {
78+
connection.setChunkedStreamingMode(8196);
79+
}
80+
connection.setDoOutput(true);
81+
new ByteSink() {
82+
public OutputStream openStream() throws IOException {
83+
return connection.getOutputStream();
84+
}
85+
}.asCharSink(UTF_8).write(request.body().get());
86+
}
87+
return connection;
88+
}
89+
90+
Response convertResponse(HttpURLConnection connection) throws IOException {
91+
int status = connection.getResponseCode();
92+
String reason = connection.getResponseMessage();
93+
94+
ImmutableListMultimap.Builder<String, String> headers = ImmutableListMultimap.builder();
95+
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
96+
// response message
97+
if (field.getKey() != null)
98+
headers.putAll(field.getKey(), field.getValue());
99+
}
100+
101+
Integer length = connection.getContentLength();
102+
if (length == -1)
103+
length = null;
104+
InputStream stream;
105+
if (status >= 400) {
106+
stream = connection.getErrorStream();
107+
} else {
108+
stream = connection.getInputStream();
109+
}
110+
Reader body = stream != null ? new InputStreamReader(stream) : null;
111+
return Response.create(status, reason, headers.build(), body, length);
112+
}
113+
}
114+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package feign;
2+
3+
import com.google.common.base.Joiner;
4+
import com.google.common.collect.ImmutableList;
5+
import com.google.common.collect.ImmutableSet;
6+
import com.google.common.reflect.TypeToken;
7+
8+
import java.lang.annotation.Annotation;
9+
import java.lang.reflect.Method;
10+
import java.net.URI;
11+
12+
import javax.ws.rs.Consumes;
13+
import javax.ws.rs.FormParam;
14+
import javax.ws.rs.HeaderParam;
15+
import javax.ws.rs.HttpMethod;
16+
import javax.ws.rs.Path;
17+
import javax.ws.rs.PathParam;
18+
import javax.ws.rs.Produces;
19+
import javax.ws.rs.QueryParam;
20+
21+
import static com.google.common.base.Preconditions.checkState;
22+
import static com.google.common.net.HttpHeaders.ACCEPT;
23+
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
24+
25+
/**
26+
* Defines what annotations and values are valid on interfaces.
27+
*/
28+
public final class Contract {
29+
30+
public static ImmutableSet<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
31+
ImmutableSet.Builder<MethodMetadata> builder = ImmutableSet.builder();
32+
for (Method method : declaring.getDeclaredMethods()) {
33+
if (method.getDeclaringClass() == Object.class)
34+
continue;
35+
builder.add(parseAndValidatateMetadata(method));
36+
}
37+
return builder.build();
38+
}
39+
40+
public static MethodMetadata parseAndValidatateMetadata(Method method) {
41+
MethodMetadata data = new MethodMetadata();
42+
data.returnType(TypeToken.of(method.getGenericReturnType()));
43+
data.configKey(Feign.configKey(method));
44+
45+
for (Annotation methodAnnotation : method.getAnnotations()) {
46+
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
47+
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
48+
if (http != null) {
49+
checkState(data.template().method() == null,
50+
"Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template()
51+
.method(), http.value());
52+
data.template().method(http.value());
53+
} else if (annotationType == RequestTemplate.Body.class) {
54+
String body = RequestTemplate.Body.class.cast(methodAnnotation).value();
55+
if (body.indexOf('{') == -1) {
56+
data.template().body(body);
57+
} else {
58+
data.template().bodyTemplate(body);
59+
}
60+
} else if (annotationType == Path.class) {
61+
data.template().append(Path.class.cast(methodAnnotation).value());
62+
} else if (annotationType == Produces.class) {
63+
data.template().header(CONTENT_TYPE, Joiner.on(',').join(((Produces) methodAnnotation).value()));
64+
} else if (annotationType == Consumes.class) {
65+
data.template().header(ACCEPT, Joiner.on(',').join(((Consumes) methodAnnotation).value()));
66+
}
67+
}
68+
checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)",
69+
method.getName());
70+
Class<?>[] parameterTypes = method.getParameterTypes();
71+
72+
Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations();
73+
int count = parameterAnnotationArrays.length;
74+
for (int i = 0; i < count; i++) {
75+
boolean hasHttpAnnotation = false;
76+
77+
Class<?> parameterType = parameterTypes[i];
78+
Annotation[] parameterAnnotations = parameterAnnotationArrays[i];
79+
if (parameterAnnotations != null) {
80+
for (Annotation parameterAnnotation : parameterAnnotations) {
81+
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
82+
if (annotationType == PathParam.class) {
83+
data.indexToName().put(i, PathParam.class.cast(parameterAnnotation).value());
84+
hasHttpAnnotation = true;
85+
} else if (annotationType == QueryParam.class) {
86+
String name = QueryParam.class.cast(parameterAnnotation).value();
87+
data.template().query(
88+
name,
89+
ImmutableList.<String>builder().addAll(data.template().queries().get(name))
90+
.add(String.format("{%s}", name)).build());
91+
data.indexToName().put(i, name);
92+
hasHttpAnnotation = true;
93+
} else if (annotationType == HeaderParam.class) {
94+
String name = HeaderParam.class.cast(parameterAnnotation).value();
95+
data.template().header(
96+
name,
97+
ImmutableList.<String>builder().addAll(data.template().headers().get(name))
98+
.add(String.format("{%s}", name)).build());
99+
data.indexToName().put(i, name);
100+
hasHttpAnnotation = true;
101+
} else if (annotationType == FormParam.class) {
102+
String form = FormParam.class.cast(parameterAnnotation).value();
103+
data.formParams().add(form);
104+
data.indexToName().put(i, form);
105+
hasHttpAnnotation = true;
106+
}
107+
}
108+
}
109+
110+
if (parameterType == URI.class) {
111+
data.urlIndex(i);
112+
} else if (!hasHttpAnnotation) {
113+
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters.");
114+
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
115+
data.bodyIndex(i);
116+
}
117+
}
118+
return data;
119+
}
120+
}

0 commit comments

Comments
 (0)