Browse Source

Support for Apache HttpClient as Feign client

pull/237/head
Tristan Burch 10 years ago committed by Adrian Cole
parent
commit
1705c15046
  1. 3
      CHANGELOG.md
  2. 12
      httpclient/README.md
  3. 12
      httpclient/build.gradle
  4. 207
      httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java
  5. 109
      httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java
  6. 2
      settings.gradle

3
CHANGELOG.md

@ -1,3 +1,6 @@
### Version 8.3
* Adds client implementation for Apache Http Client
### Version 8.2 ### Version 8.2
* Allows customized request construction by exposing `Request.create()` * Allows customized request construction by exposing `Request.create()`
* Adds JMH benchmark module * Adds JMH benchmark module

12
httpclient/README.md

@ -0,0 +1,12 @@
Apache HttpClient
===================
This module directs Feign's http requests to Apache's [HttpClient](https://hc.apache.org/httpcomponents-client-ga/).
To use HttpClient with Feign, add the HttpClient module to your classpath. Then, configure Feign to use the HttpClient:
```java
GitHub github = Feign.builder()
.client(new ApacheHttpClient())
.target(GitHub.class, "https://api.github.com");
```

12
httpclient/build.gradle

@ -0,0 +1,12 @@
apply plugin: 'java'
sourceCompatibility = 1.6
dependencies {
compile project(':feign-core')
compile 'org.apache.httpcomponents:httpclient:4.4.1'
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
}

207
httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java

@ -0,0 +1,207 @@
/*
* 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.httpclient;
import feign.Client;
import feign.Request;
import feign.Response;
import feign.Util;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This module directs Feign's http requests to Apache's
* <a href="https://hc.apache.org/httpcomponents-client-ga/">HttpClient</a>. Ex.
* <pre>
* GitHub github = Feign.builder().client(new ApacheHttpClient()).target(GitHub.class,
* "https://api.github.com");
*/
/*
* Based on Square, Inc's Retrofit ApacheClient implementation
*/
public final class ApacheHttpClient implements Client {
private static final String ACCEPT_HEADER_NAME = "Accept";
private final HttpClient client;
public ApacheHttpClient() {
this(HttpClientBuilder.create().build());
}
public ApacheHttpClient(HttpClient client) {
this.client = client;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
HttpUriRequest httpUriRequest;
try {
httpUriRequest = toHttpUriRequest(request, options);
} catch (URISyntaxException e) {
throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
}
HttpResponse httpResponse = client.execute(httpUriRequest);
return toFeignResponse(httpResponse);
}
HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws
UnsupportedEncodingException, MalformedURLException, URISyntaxException {
RequestBuilder requestBuilder = RequestBuilder.create(request.method());
//per request timeouts
RequestConfig requestConfig = RequestConfig
.custom()
.setConnectTimeout(options.connectTimeoutMillis())
.setSocketTimeout(options.readTimeoutMillis())
.build();
requestBuilder.setConfig(requestConfig);
URI uri = new URIBuilder(request.url()).build();
//request url
requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getPath());
//request query params
List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());
for (NameValuePair queryParam: queryParams) {
requestBuilder.addParameter(queryParam);
}
//request body
if (request.body() != null) {
HttpEntity entity = request.charset() != null ?
new StringEntity(new String(request.body(), request.charset())) :
new ByteArrayEntity(request.body());
requestBuilder.setEntity(entity);
}
//request headers
boolean hasAcceptHeader = false;
for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
String headerName = headerEntry.getKey();
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
hasAcceptHeader = true;
}
if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH) &&
requestBuilder.getHeaders(headerName) != null) {
//if the 'Content-Length' header is already present, it's been set from HttpEntity, so we
//won't add it again
continue;
}
for (String headerValue : headerEntry.getValue()) {
requestBuilder.addHeader(headerName, headerValue);
}
}
//some servers choke on the default accept string, so we'll set it to anything
if (!hasAcceptHeader) {
requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*");
}
return requestBuilder.build();
}
Response toFeignResponse(HttpResponse httpResponse) throws IOException {
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
String reason = statusLine.getReasonPhrase();
Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>();
for (Header header : httpResponse.getAllHeaders()) {
String name = header.getName();
String value = header.getValue();
Collection<String> headerValues = headers.get(name);
if (headerValues == null) {
headerValues = new ArrayList<String>();
headers.put(name, headerValues);
}
headerValues.add(value);
}
return Response.create(statusCode, reason, headers, toFeignBody(httpResponse));
}
Response.Body toFeignBody(HttpResponse httpResponse) throws IOException {
HttpEntity entity = httpResponse.getEntity();
final Integer length = entity != null && entity.getContentLength() != -1 ?
(int) entity.getContentLength() :
null;
final InputStream input = entity != null ?
new ByteArrayInputStream(EntityUtils.toByteArray(entity)) :
null;
return new Response.Body() {
@Override
public void close() throws IOException {
if (input != null) {
input.close();
}
}
@Override
public Integer length() {
return length;
}
@Override
public boolean isRepeatable() {
return false;
}
@Override
public InputStream asInputStream() throws IOException {
return input;
}
@Override
public Reader asReader() throws IOException {
return new InputStreamReader(input);
}
};
}
}

109
httpclient/src/test/java/feign/httpclient/ApacheHttpClientTest.java

@ -0,0 +1,109 @@
/*
* 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.httpclient;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import feign.Feign;
import feign.FeignException;
import feign.Headers;
import feign.RequestLine;
import feign.Response;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import static feign.Util.UTF_8;
import static feign.assertj.MockWebServerAssertions.assertThat;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
public class ApacheHttpClientTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final MockWebServerRule server = new MockWebServerRule();
@Test
public void parsesRequestAndResponse() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
TestInterface api = Feign.builder()
.client(new ApacheHttpClient())
.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: ", "Accept: */*", "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 ApacheHttpClient())
.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 ApacheHttpClient())
.target(TestInterface.class, "http://localhost:" + server.getPort());
assertEquals("foo", api.patch());
assertThat(server.takeRequest())
.hasHeaders("Accept: text/plain")
.hasNoHeaderNamed("Content-Type")
.hasMethod("PATCH");
}
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 /")
@Headers("Accept: text/plain")
String patch();
}
}

2
settings.gradle

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

Loading…
Cancel
Save