Skip to content

Commit 6a343c7

Browse files
authored
Async client for apache http 5 (OpenFeign#1179)
1 parent 40c026e commit 6a343c7

5 files changed

Lines changed: 1251 additions & 4 deletions

File tree

core/src/main/java/feign/Feign.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,12 @@ public Feign build() {
272272
}
273273
}
274274

275-
static class ResponseMappingDecoder implements Decoder {
275+
public static class ResponseMappingDecoder implements Decoder {
276276

277277
private final ResponseMapper mapper;
278278
private final Decoder delegate;
279279

280-
ResponseMappingDecoder(ResponseMapper mapper, Decoder decoder) {
280+
public ResponseMappingDecoder(ResponseMapper mapper, Decoder decoder) {
281281
this.mapper = mapper;
282282
this.delegate = decoder;
283283
}

hc5/pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
</parent>
2525

2626
<artifactId>feign-hc5</artifactId>
27-
<name>Feign Apache Http Client 5</name>
27+
<name>Feign Apache HttpComponents Client 5</name>
2828
<description>Feign Apache HttpComponents Client 5</description>
2929

3030
<properties>
@@ -40,7 +40,7 @@
4040
<dependency>
4141
<groupId>org.apache.httpcomponents.client5</groupId>
4242
<artifactId>httpclient5</artifactId>
43-
<version>5.0-beta5</version>
43+
<version>5.0-beta7</version>
4444
</dependency>
4545

4646
<dependency>
@@ -56,6 +56,12 @@
5656
<scope>test</scope>
5757
</dependency>
5858

59+
<dependency>
60+
<groupId>com.google.code.gson</groupId>
61+
<artifactId>gson</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
5965
<dependency>
6066
<groupId>com.squareup.okhttp3</groupId>
6167
<artifactId>mockwebserver</artifactId>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* Copyright 2012-2020 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.hc5;
15+
16+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
17+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
18+
import org.apache.hc.client5.http.config.Configurable;
19+
import org.apache.hc.client5.http.config.RequestConfig;
20+
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
21+
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
22+
import org.apache.hc.client5.http.protocol.HttpClientContext;
23+
import org.apache.hc.core5.concurrent.FutureCallback;
24+
import org.apache.hc.core5.http.ContentType;
25+
import org.apache.hc.core5.http.Header;
26+
import org.apache.hc.core5.io.CloseMode;
27+
import java.util.*;
28+
import java.util.concurrent.CompletableFuture;
29+
import feign.*;
30+
import feign.Request.Options;
31+
32+
/**
33+
* This module directs Feign's http requests to Apache's
34+
* <a href="https://hc.apache.org/httpcomponents-client-5.0.x/index.html">HttpClient 5</a>. Ex.
35+
*
36+
* <pre>
37+
* GitHub github = Feign.builder().client(new ApacheHttp5Client()).target(GitHub.class,
38+
* "https://api.github.com");
39+
*/
40+
/*
41+
*/
42+
public final class AsyncApacheHttp5Client implements AsyncClient<HttpClientContext>, AutoCloseable {
43+
44+
private static final String ACCEPT_HEADER_NAME = "Accept";
45+
46+
private final CloseableHttpAsyncClient client;
47+
48+
public AsyncApacheHttp5Client() {
49+
this(createStartedClient());
50+
}
51+
52+
private static CloseableHttpAsyncClient createStartedClient() {
53+
final CloseableHttpAsyncClient client = HttpAsyncClients.custom().build();
54+
client.start();
55+
return client;
56+
}
57+
58+
public AsyncApacheHttp5Client(CloseableHttpAsyncClient client) {
59+
this.client = client;
60+
}
61+
62+
@Override
63+
public CompletableFuture<Response> execute(Request request,
64+
Options options,
65+
Optional<HttpClientContext> requestContext) {
66+
final SimpleHttpRequest httpUriRequest = toClassicHttpRequest(request, options);
67+
68+
final CompletableFuture<Response> result = new CompletableFuture<>();
69+
final FutureCallback<SimpleHttpResponse> callback = new FutureCallback<SimpleHttpResponse>() {
70+
71+
@Override
72+
public void completed(SimpleHttpResponse httpResponse) {
73+
result.complete(toFeignResponse(httpResponse, request));
74+
}
75+
76+
@Override
77+
public void failed(Exception ex) {
78+
result.completeExceptionally(ex);
79+
}
80+
81+
@Override
82+
public void cancelled() {
83+
result.cancel(false);
84+
}
85+
};
86+
87+
client.execute(httpUriRequest,
88+
configureTimeouts(options, requestContext.orElseGet(HttpClientContext::new)),
89+
callback);
90+
91+
return result;
92+
}
93+
94+
protected HttpClientContext configureTimeouts(Request.Options options,
95+
HttpClientContext context) {
96+
// per request timeouts
97+
final RequestConfig requestConfig =
98+
(client instanceof Configurable
99+
? RequestConfig.copy(((Configurable) client).getConfig())
100+
: RequestConfig.custom())
101+
.setConnectTimeout(options.connectTimeout(), options.connectTimeoutUnit())
102+
.setResponseTimeout(options.readTimeout(), options.readTimeoutUnit())
103+
.build();
104+
context.setRequestConfig(requestConfig);
105+
return context;
106+
}
107+
108+
SimpleHttpRequest toClassicHttpRequest(Request request,
109+
Request.Options options) {
110+
final SimpleHttpRequest httpRequest =
111+
new SimpleHttpRequest(request.httpMethod().name(), request.url());
112+
113+
// request headers
114+
boolean hasAcceptHeader = false;
115+
for (final Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
116+
final String headerName = headerEntry.getKey();
117+
if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
118+
hasAcceptHeader = true;
119+
}
120+
121+
if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
122+
// The 'Content-Length' header is always set by the Apache client and it
123+
// doesn't like us to set it as well.
124+
continue;
125+
}
126+
127+
for (final String headerValue : headerEntry.getValue()) {
128+
httpRequest.addHeader(headerName, headerValue);
129+
}
130+
}
131+
// some servers choke on the default accept string, so we'll set it to anything
132+
if (!hasAcceptHeader) {
133+
httpRequest.addHeader(ACCEPT_HEADER_NAME, "*/*");
134+
}
135+
136+
// request body
137+
// final Body requestBody = request.requestBody();
138+
final byte[] data = request.body();
139+
if (data != null) {
140+
httpRequest.setBodyBytes(data, getContentType(request));
141+
}
142+
143+
return httpRequest;
144+
}
145+
146+
private ContentType getContentType(Request request) {
147+
ContentType contentType = null;
148+
for (final Map.Entry<String, Collection<String>> entry : request.headers().entrySet()) {
149+
if (entry.getKey().equalsIgnoreCase("Content-Type")) {
150+
final Collection<String> values = entry.getValue();
151+
if (values != null && !values.isEmpty()) {
152+
contentType = ContentType.parse(values.iterator().next());
153+
if (contentType.getCharset() == null) {
154+
contentType = contentType.withCharset(request.charset());
155+
}
156+
break;
157+
}
158+
}
159+
}
160+
return contentType;
161+
}
162+
163+
Response toFeignResponse(SimpleHttpResponse httpResponse, Request request) {
164+
final int statusCode = httpResponse.getCode();
165+
166+
final String reason = httpResponse.getReasonPhrase();
167+
168+
final Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>();
169+
for (final Header header : httpResponse.getHeaders()) {
170+
final String name = header.getName();
171+
final String value = header.getValue();
172+
173+
Collection<String> headerValues = headers.get(name);
174+
if (headerValues == null) {
175+
headerValues = new ArrayList<String>();
176+
headers.put(name, headerValues);
177+
}
178+
headerValues.add(value);
179+
}
180+
181+
return Response.builder()
182+
.status(statusCode)
183+
.reason(reason)
184+
.headers(headers)
185+
.request(request)
186+
.body(httpResponse
187+
.getBodyBytes())
188+
.build();
189+
}
190+
191+
@Override
192+
public void close() throws Exception {
193+
client.close(CloseMode.GRACEFUL);
194+
}
195+
196+
}

0 commit comments

Comments
 (0)