Skip to content

Commit

Permalink
initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
adriancole committed Jun 27, 2013
1 parent 54f3dd4 commit 222fc12
Show file tree
Hide file tree
Showing 35 changed files with 3,549 additions and 27 deletions.
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ Thumbs.db
# Gradle Files #
################
.gradle
local.properties

# Build output directies
/target
*/target
/build
**/test-output
**/target
**/bin
build
*/build
.m2

# IntelliJ specific files/directories
out
Expand Down
102 changes: 100 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,100 @@
feign
=====
# Feign makes writing java http clients easier
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).

### Why Feign and not X?

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.

### How does Feign work?

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.

### Basics

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).

```java
interface GitHub {
@GET @Path("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
}

static class Contributor {
String login;
int contributions;
}

public static void main(String... args) {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());

// Fetch and print a list of the contributors to this library.
List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
```
### Decoders
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.

```java
@Module(overrides = true, library = true)
static class GsonModule {
@Provides @Singleton Map<String, Decoder> decoders() {
return ImmutableMap.of("GitHub", gsonDecoder);
}

final Decoder gsonDecoder = new Decoder() {
Gson gson = new Gson();

@Override public Object decode(String methodKey, Reader reader, TypeToken<?> type) {
return gson.fromJson(reader, type.getType());
}
};
}
```
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.

### Multiple Interfaces
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.

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<CloudDNS>(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!
### Advanced usage and Dagger
#### Dagger
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.

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:
```java
@Provides @Singleton Map<String, Decoder> decoders() {
return ImmutableMap.of("GitHub", gsonDecoder);
}
```
#### Wire Logging
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:
```java
@Module(overrides = true)
class Overrides {
@Provides @Singleton Wire provideWire() {
return new Wire.LoggingWire().appendToFile("logs/http-wire.log");
}
}
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides());
```
#### Pattern Decoders
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.

Here's how our IAM example grabs only one xml element from a response.
```java
@Module(overrides = true, library = true)
static class IAMModule {
@Provides @Singleton Map<String, Decoder> decoders() {
return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>"));
}
}
```

30 changes: 10 additions & 20 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,19 @@ apply from: file('gradle/release.gradle')

subprojects {
group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project

dependencies {
compile 'javax.ws.rs:jsr311-api:1.1.1'
compile 'com.sun.jersey:jersey-core:1.11'
testCompile 'org.testng:testng:6.1.1'
testCompile 'org.mockito:mockito-core:1.8.5'
}
}

project(':template-client') {
project(':feign-core') {
apply plugin: 'java'
dependencies {
compile 'org.slf4j:slf4j-api:1.6.3'
compile 'com.sun.jersey:jersey-client:1.11'
}
}

project(':template-server') {
apply plugin: 'war'
apply plugin: 'jetty'
dependencies {
compile 'com.sun.jersey:jersey-server:1.11'
compile 'com.sun.jersey:jersey-servlet:1.11'
compile project(':template-client')
}
compile 'com.google.guava:guava:14.0.1'
compile 'com.squareup.dagger:dagger:1.0.1'
compile 'javax.ws.rs:jsr311-api:1.1.1'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
testCompile 'com.google.code.gson:gson:2.2.4'
testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2'
testCompile 'org.testng:testng:6.8.1'
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
}
}
114 changes: 114 additions & 0 deletions feign-core/src/main/java/feign/Client.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package feign;

import com.google.common.collect.ImmutableListMultimap;
import com.google.common.io.ByteSink;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.inject.Inject;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

import dagger.Lazy;
import feign.Request.Options;

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;

/**
* Submits HTTP {@link Request requests}. Implementations are expected to be
* thread-safe.
*/
public interface Client {
/**
* Executes a request against its {@link Request#url() url} and returns a
* response.
*
* @param request safe to replay.
* @param options options to apply to this request.
* @return connected response, {@link Response.Body} is absent or unread.
* @throws IOException on a network error connecting to {@link Request#url()}.
*/
Response execute(Request request, Options options) throws IOException;

public static class Default implements Client {
private final Lazy<SSLSocketFactory> sslContextFactory;

@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory) {
this.sslContextFactory = sslContextFactory;
}

@Override public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection);
}

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
sslCon.setSSLSocketFactory(sslContextFactory.get());
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod(request.method());

Integer contentLength = null;
for (Entry<String, String> header : request.headers().entries()) {
if (header.getKey().equals(CONTENT_LENGTH))
contentLength = Integer.valueOf(header.getValue());
connection.addRequestProperty(header.getKey(), header.getValue());
}

if (request.body().isPresent()) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
new ByteSink() {
public OutputStream openStream() throws IOException {
return connection.getOutputStream();
}
}.asCharSink(UTF_8).write(request.body().get());
}
return connection;
}

Response convertResponse(HttpURLConnection connection) throws IOException {
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();

ImmutableListMultimap.Builder<String, String> headers = ImmutableListMultimap.builder();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null)
headers.putAll(field.getKey(), field.getValue());
}

Integer length = connection.getContentLength();
if (length == -1)
length = null;
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
Reader body = stream != null ? new InputStreamReader(stream) : null;
return Response.create(status, reason, headers.build(), body, length);
}
}
}
120 changes: 120 additions & 0 deletions feign-core/src/main/java/feign/Contract.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package feign;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.TypeToken;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URI;

import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.net.HttpHeaders.ACCEPT;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;

/**
* Defines what annotations and values are valid on interfaces.
*/
public final class Contract {

public static ImmutableSet<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
ImmutableSet.Builder<MethodMetadata> builder = ImmutableSet.builder();
for (Method method : declaring.getDeclaredMethods()) {
if (method.getDeclaringClass() == Object.class)
continue;
builder.add(parseAndValidatateMetadata(method));
}
return builder.build();
}

public static MethodMetadata parseAndValidatateMetadata(Method method) {
MethodMetadata data = new MethodMetadata();
data.returnType(TypeToken.of(method.getGenericReturnType()));
data.configKey(Feign.configKey(method));

for (Annotation methodAnnotation : method.getAnnotations()) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
if (http != null) {
checkState(data.template().method() == null,
"Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template()
.method(), http.value());
data.template().method(http.value());
} else if (annotationType == RequestTemplate.Body.class) {
String body = RequestTemplate.Body.class.cast(methodAnnotation).value();
if (body.indexOf('{') == -1) {
data.template().body(body);
} else {
data.template().bodyTemplate(body);
}
} else if (annotationType == Path.class) {
data.template().append(Path.class.cast(methodAnnotation).value());
} else if (annotationType == Produces.class) {
data.template().header(CONTENT_TYPE, Joiner.on(',').join(((Produces) methodAnnotation).value()));
} else if (annotationType == Consumes.class) {
data.template().header(ACCEPT, Joiner.on(',').join(((Consumes) methodAnnotation).value()));
}
}
checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)",
method.getName());
Class<?>[] parameterTypes = method.getParameterTypes();

Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations();
int count = parameterAnnotationArrays.length;
for (int i = 0; i < count; i++) {
boolean hasHttpAnnotation = false;

Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = parameterAnnotationArrays[i];
if (parameterAnnotations != null) {
for (Annotation parameterAnnotation : parameterAnnotations) {
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
if (annotationType == PathParam.class) {
data.indexToName().put(i, PathParam.class.cast(parameterAnnotation).value());
hasHttpAnnotation = true;
} else if (annotationType == QueryParam.class) {
String name = QueryParam.class.cast(parameterAnnotation).value();
data.template().query(
name,
ImmutableList.<String>builder().addAll(data.template().queries().get(name))
.add(String.format("{%s}", name)).build());
data.indexToName().put(i, name);
hasHttpAnnotation = true;
} else if (annotationType == HeaderParam.class) {
String name = HeaderParam.class.cast(parameterAnnotation).value();
data.template().header(
name,
ImmutableList.<String>builder().addAll(data.template().headers().get(name))
.add(String.format("{%s}", name)).build());
data.indexToName().put(i, name);
hasHttpAnnotation = true;
} else if (annotationType == FormParam.class) {
String form = FormParam.class.cast(parameterAnnotation).value();
data.formParams().add(form);
data.indexToName().put(i, form);
hasHttpAnnotation = true;
}
}
}

if (parameterType == URI.class) {
data.urlIndex(i);
} else if (!hasHttpAnnotation) {
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters.");
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
}
}
return data;
}
}
Loading

0 comments on commit 222fc12

Please sign in to comment.