Browse Source

Adds OkHttp integration

closes #134
pull/147/head
Adrian Cole 10 years ago
parent
commit
49b700e6f7
  1. 1
      CHANGELOG.md
  2. 11
      README.md
  3. 4
      core/src/main/java/feign/Response.java
  4. 2
      core/src/test/java/feign/codec/DefaultDecoderTest.java
  5. 4
      core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
  6. 2
      gson/src/test/java/feign/gson/GsonModuleTest.java
  7. 2
      jackson/src/test/java/feign/jackson/JacksonModuleTest.java
  8. 12
      okhttp/README.md
  9. 12
      okhttp/build.gradle
  10. 131
      okhttp/src/main/java/feign/okhttp/OkHttpClient.java
  11. 103
      okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
  12. 2
      sax/src/test/java/feign/sax/SAXDecoderTest.java
  13. 2
      settings.gradle

1
CHANGELOG.md

@ -1,5 +1,6 @@
### Version 7.1 ### Version 7.1
* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
* Adds OkHttp integration
* Allows multiple headers with the same name. * Allows multiple headers with the same name.
### Version 7.0 ### Version 7.0

11
README.md

@ -150,6 +150,17 @@ GitHub github = Feign.builder()
.contract(new JAXRSModule.JAXRSContract()) .contract(new JAXRSModule.JAXRSContract())
.target(GitHub.class, "https://api.github.com"); .target(GitHub.class, "https://api.github.com");
``` ```
### OkHttp
[OkHttpClient](https://github.com/Netflix/feign/tree/master/okhttp) directs Feign's http requests to [OkHttp](http://square.github.io/okhttp/), which enables SPDY and better network control.
To use OkHttp with Feign, add the OkHttp module to your classpath. Then, configure Feign to use the OkHttpClient:
```java
GitHub github = Feign.builder()
.client(new OkHttpClient())
.target(GitHub.class, "https://api.github.com");
```
### Ribbon ### Ribbon
[RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). [RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon).

4
core/src/main/java/feign/Response.java

@ -58,6 +58,10 @@ public final class Response {
return new Response(status, reason, headers, ByteArrayBody.orNull(text, charset)); return new Response(status, reason, headers, ByteArrayBody.orNull(text, charset));
} }
public static Response create(int status, String reason, Map<String, Collection<String>> headers, Body body) {
return new Response(status, reason, headers, body);
}
private Response(int status, String reason, Map<String, Collection<String>> headers, Body body) { private Response(int status, String reason, Map<String, Collection<String>> headers, Body body) {
checkState(status >= 200, "Invalid status code: %s", status); checkState(status >= 200, "Invalid status code: %s", status);
this.status = status; this.status = status;

2
core/src/test/java/feign/codec/DefaultDecoderTest.java

@ -70,6 +70,6 @@ public class DefaultDecoderTest {
} }
private Response nullBodyResponse() { private Response nullBodyResponse() {
return Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), null); return Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
} }
} }

4
core/src/test/java/feign/codec/DefaultErrorDecoderTest.java

@ -39,7 +39,7 @@ public class DefaultErrorDecoderTest {
thrown.expect(FeignException.class); thrown.expect(FeignException.class);
thrown.expectMessage("status 500 reading Service#foo()"); thrown.expectMessage("status 500 reading Service#foo()");
Response response = Response.create(500, "Internal server error", headers, null); Response response = Response.create(500, "Internal server error", headers, (byte[]) null);
throw errorDecoder.decode("Service#foo()", response); throw errorDecoder.decode("Service#foo()", response);
} }
@ -58,7 +58,7 @@ public class DefaultErrorDecoderTest {
thrown.expectMessage("status 503 reading Service#foo()"); thrown.expectMessage("status 503 reading Service#foo()");
headers.put(RETRY_AFTER, Arrays.asList("Sat, 1 Jan 2000 00:00:00 GMT")); headers.put(RETRY_AFTER, Arrays.asList("Sat, 1 Jan 2000 00:00:00 GMT"));
Response response = Response.create(503, "Service Unavailable", headers, null); Response response = Response.create(503, "Service Unavailable", headers, (byte[]) null);
throw errorDecoder.decode("Service#foo()", response); throw errorDecoder.decode("Service#foo()", response);
} }

2
gson/src/test/java/feign/gson/GsonModuleTest.java

@ -141,7 +141,7 @@ public class GsonModuleTest {
DecoderBindings bindings = new DecoderBindings(); DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings); ObjectGraph.create(bindings).inject(bindings);
Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null); Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(bindings.decoder.decode(response, String.class)); assertNull(bindings.decoder.decode(response, String.class));
} }

2
jackson/src/test/java/feign/jackson/JacksonModuleTest.java

@ -128,7 +128,7 @@ public class JacksonModuleTest {
DecoderBindings bindings = new DecoderBindings(); DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings); ObjectGraph.create(bindings).inject(bindings);
Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null); Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(bindings.decoder.decode(response, String.class)); assertNull(bindings.decoder.decode(response, String.class));
} }

12
okhttp/README.md

@ -0,0 +1,12 @@
OkHttp
===================
This module directs Feign's http requests to [OkHttp](http://square.github.io/okhttp/), which enables SPDY and better network control.
To use OkHttp with Feign, add the OkHttp module to your classpath. Then, configure Feign to use the OkHttpClient:
```java
GitHub github = Feign.builder()
.client(new OkHttpClient())
.target(GitHub.class, "https://api.github.com");
```

12
okhttp/build.gradle

@ -0,0 +1,12 @@
apply plugin: 'java'
sourceCompatibility = 1.6
dependencies {
compile project(':feign-core')
compile 'com.squareup.okhttp:okhttp:2.2.0'
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'com.squareup.okhttp:mockwebserver:2.2.0'
testCompile project(':feign-core').sourceSets.test.output // for assertions
}

131
okhttp/src/main/java/feign/okhttp/OkHttpClient.java

@ -0,0 +1,131 @@
/*
* Copyright 2015 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.okhttp;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import feign.Client;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* This module directs Feign's http requests to <a href="http://square.github.io/okhttp/">OkHttp</a>, which enables
* SPDY and better network control.
* Ex.
* <pre>
* GitHub github = Feign.builder().client(new OkHttpClient()).target(GitHub.class, "https://api.github.com");
*/
public final class OkHttpClient implements Client {
private final com.squareup.okhttp.OkHttpClient delegate;
public OkHttpClient() {
this(new com.squareup.okhttp.OkHttpClient());
}
public OkHttpClient(com.squareup.okhttp.OkHttpClient delegate) {
this.delegate = delegate;
}
@Override public feign.Response execute(feign.Request input, feign.Request.Options options) throws IOException {
com.squareup.okhttp.OkHttpClient requestScoped;
if (delegate.getConnectTimeout() != options.connectTimeoutMillis()
|| delegate.getReadTimeout() != options.readTimeoutMillis()) {
requestScoped = delegate.clone();
requestScoped.setConnectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS);
requestScoped.setReadTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS);
} else {
requestScoped = delegate;
}
Request request = toOkHttpRequest(input);
Response response = requestScoped.newCall(request).execute();
return toFeignResponse(response);
}
static Request toOkHttpRequest(feign.Request input) {
Request.Builder requestBuilder = new Request.Builder();
requestBuilder.url(input.url());
MediaType mediaType = null;
for (String field : input.headers().keySet()) {
for (String value : input.headers().get(field)) {
if (field.equalsIgnoreCase("Content-Type")) {
mediaType = MediaType.parse(value);
if (input.charset() != null) mediaType.charset(input.charset());
} else {
requestBuilder.addHeader(field, value);
}
}
}
RequestBody body = input.body() != null ? RequestBody.create(mediaType, input.body()) : null;
requestBuilder.method(input.method(), body);
return requestBuilder.build();
}
private static feign.Response toFeignResponse(Response input) {
return feign.Response.create(input.code(), input.message(), toMap(input.headers()), toBody(input.body()));
}
private static Map<String, Collection<String>> toMap(Headers headers) {
Map<String, Collection<String>> result = new LinkedHashMap<String, Collection<String>>(headers.size());
for (String name : headers.names()) {
// TODO: this is very inefficient as headers.values iterate case insensitively.
result.put(name, headers.values(name));
}
return result;
}
private static feign.Response.Body toBody(final ResponseBody input) {
if (input == null || input.contentLength() == 0) {
return null;
}
if (input.contentLength() > Integer.MAX_VALUE) {
throw new UnsupportedOperationException("Length too long "+ input.contentLength());
}
final Integer length = input.contentLength() != -1 ? (int) input.contentLength() : null;
return new feign.Response.Body() {
@Override public void close() throws IOException {
input.close();
}
@Override public Integer length() {
return length;
}
@Override public boolean isRepeatable() {
return false;
}
@Override public InputStream asInputStream() throws IOException {
return input.byteStream();
}
@Override public Reader asReader() throws IOException {
return input.charStream();
}
};
}
}

103
okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java

@ -0,0 +1,103 @@
/*
* Copyright 2015 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.okhttp;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Lazy;
import feign.Client;
import feign.Feign;
import feign.FeignException;
import feign.Headers;
import feign.RequestLine;
import feign.Response;
import feign.Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static feign.Util.UTF_8;
import static feign.assertj.MockWebServerAssertions.assertThat;
import static java.util.Arrays.asList;
import static org.assertj.core.data.MapEntry.entry;
import static org.hamcrest.core.Is.isA;
import static org.junit.Assert.assertEquals;
public class OkHttpClientTest {
@Rule public final ExpectedException thrown = ExpectedException.none();
@Rule public final MockWebServerRule server = new MockWebServerRule();
interface TestInterface {
@RequestLine("POST /?foo=bar&foo=baz&qux=")
@Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body);
@RequestLine("PATCH /") String patch();
}
@Test public void parsesRequestAndResponse() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
TestInterface api = Feign.builder()
.client(new OkHttpClient())
.target(TestInterface.class, "http://localhost:" + server.getPort());
Response response = api.post("foo");
assertThat(response.status()).isEqualTo(200);
assertThat(response.reason()).isEqualTo("OK");
assertThat(response.headers())
.containsEntry("Content-Length", asList("3"))
.containsEntry("Foo", asList("Bar"));
assertThat(response.body().asInputStream()).hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8)));
assertThat(server.takeRequest()).hasMethod("POST")
.hasPath("/?foo=bar&foo=baz&qux=")
.hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Content-Length: 3")
.hasBody("foo");
}
@Test public void parsesErrorResponse() throws IOException, InterruptedException {
thrown.expect(FeignException.class);
thrown.expectMessage("status 500 reading TestInterface#post(String); content:\n" + "ARGHH");
server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH"));
TestInterface api = Feign.builder()
.client(new OkHttpClient())
.target(TestInterface.class, "http://localhost:" + server.getPort());
api.post("foo");
}
@Test public void patch() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
server.enqueue(new MockResponse());
TestInterface api = Feign.builder()
.client(new OkHttpClient())
.target(TestInterface.class, "http://localhost:" + server.getPort());
assertEquals("foo", api.patch());
assertThat(server.takeRequest())
.hasHeaders("Content-Length: 0") // Note: OkHttp adds content length.
.hasNoHeaderNamed("Content-Type")
.hasMethod("PATCH");
}
}

2
sax/src/test/java/feign/sax/SAXDecoderTest.java

@ -142,7 +142,7 @@ public class SAXDecoderTest {
} }
@Test public void nullBodyDecodesToNull() throws Exception { @Test public void nullBodyDecodesToNull() throws Exception {
Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null); Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(decoder.decode(response, String.class)); assertNull(decoder.decode(response, String.class));
} }
} }

2
settings.gradle

@ -1,5 +1,5 @@
rootProject.name='feign' rootProject.name='feign'
include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia' include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia'
rootProject.children.each { childProject -> rootProject.children.each { childProject ->
childProject.name = 'feign-' + childProject.name childProject.name = 'feign-' + childProject.name

Loading…
Cancel
Save