Skip to content

Commit dd57ec9

Browse files
committed
Merge pull request #30478 from mdeinum:http-client
* gh-30478: Polishing external contribution HttpClient based ClientHttpRequestFactory
2 parents c2e3fed + 0033eb4 commit dd57ec9

File tree

7 files changed

+950
-3
lines changed

7 files changed

+950
-3
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2023-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.UncheckedIOException;
22+
import java.net.URI;
23+
import java.net.http.HttpClient;
24+
import java.net.http.HttpRequest;
25+
import java.net.http.HttpResponse;
26+
import java.nio.ByteBuffer;
27+
import java.util.List;
28+
import java.util.concurrent.Executor;
29+
import java.util.concurrent.Flow;
30+
31+
import org.springframework.http.HttpHeaders;
32+
import org.springframework.http.HttpMethod;
33+
import org.springframework.lang.Nullable;
34+
import org.springframework.util.StreamUtils;
35+
36+
/**
37+
* {@link ClientHttpRequest} implementation based the Java {@link HttpClient}.
38+
* Created via the {@link JdkClientHttpRequestFactory}.
39+
*
40+
* @author Marten Deinum
41+
* @author Arjen Poutsma
42+
* @since 6.1
43+
*/
44+
class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
45+
46+
/*
47+
* The JDK HttpRequest doesn't allow all headers to be set. The named headers are taken from the default
48+
* implementation for HttpRequest.
49+
*/
50+
private static final List<String> DISALLOWED_HEADERS =
51+
List.of("connection", "content-length", "expect", "host", "upgrade");
52+
53+
private final HttpClient httpClient;
54+
55+
private final HttpMethod method;
56+
57+
private final URI uri;
58+
59+
private final Executor executor;
60+
61+
62+
public JdkClientHttpRequest(HttpClient httpClient, URI uri, HttpMethod method, Executor executor) {
63+
this.httpClient = httpClient;
64+
this.uri = uri;
65+
this.method = method;
66+
this.executor = executor;
67+
}
68+
69+
@Override
70+
public HttpMethod getMethod() {
71+
return this.method;
72+
}
73+
74+
@Override
75+
public URI getURI() {
76+
return this.uri;
77+
}
78+
79+
80+
@Override
81+
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
82+
try {
83+
HttpRequest request = buildRequest(headers, body);
84+
HttpResponse<InputStream> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
85+
return new JdkClientHttpResponse(response);
86+
}
87+
catch (UncheckedIOException ex) {
88+
throw ex.getCause();
89+
}
90+
catch (InterruptedException ex) {
91+
Thread.currentThread().interrupt();
92+
throw new IOException("Could not send request: " + ex.getMessage(), ex);
93+
}
94+
}
95+
96+
97+
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
98+
HttpRequest.Builder builder = HttpRequest.newBuilder()
99+
.uri(this.uri);
100+
101+
headers.forEach((headerName, headerValues) -> {
102+
if (!headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) {
103+
if (!DISALLOWED_HEADERS.contains(headerName.toLowerCase())) {
104+
for (String headerValue : headerValues) {
105+
builder.header(headerName, headerValue);
106+
}
107+
}
108+
}
109+
});
110+
111+
builder.method(this.method.name(), bodyPublisher(headers, body));
112+
return builder.build();
113+
}
114+
115+
private HttpRequest.BodyPublisher bodyPublisher(HttpHeaders headers, @Nullable Body body) {
116+
if (body != null) {
117+
Flow.Publisher<ByteBuffer> outputStreamPublisher = OutputStreamPublisher.create(
118+
outputStream -> body.writeTo(StreamUtils.nonClosing(outputStream)),
119+
this.executor);
120+
121+
long contentLength = headers.getContentLength();
122+
if (contentLength != -1) {
123+
return HttpRequest.BodyPublishers.fromPublisher(outputStreamPublisher, contentLength);
124+
}
125+
else {
126+
return HttpRequest.BodyPublishers.fromPublisher(outputStreamPublisher);
127+
}
128+
}
129+
else {
130+
return HttpRequest.BodyPublishers.noBody();
131+
}
132+
}
133+
134+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2023-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.io.IOException;
20+
import java.net.URI;
21+
import java.net.http.HttpClient;
22+
import java.util.concurrent.Executor;
23+
24+
import org.springframework.core.task.SimpleAsyncTaskExecutor;
25+
import org.springframework.http.HttpMethod;
26+
import org.springframework.util.Assert;
27+
28+
29+
/**
30+
* {@link ClientHttpRequestFactory} implementation based on the Java
31+
* {@link HttpClient}.
32+
*
33+
* @author Marten Deinum
34+
* @author Arjen Poutsma
35+
* @since 6.1
36+
*/
37+
public class JdkClientHttpRequestFactory implements ClientHttpRequestFactory {
38+
39+
private final HttpClient httpClient;
40+
41+
private final Executor executor;
42+
43+
44+
/**
45+
* Create a new instance of the {@code JdkClientHttpRequestFactory}
46+
* with a default {@link HttpClient}.
47+
*/
48+
public JdkClientHttpRequestFactory() {
49+
this(HttpClient.newHttpClient());
50+
}
51+
52+
/**
53+
* Create a new instance of the {@code JdkClientHttpRequestFactory} based on
54+
* the given {@link HttpClient}.
55+
* @param httpClient the client to base on
56+
*/
57+
public JdkClientHttpRequestFactory(HttpClient httpClient) {
58+
Assert.notNull(httpClient, "HttpClient is required");
59+
this.httpClient = httpClient;
60+
this.executor = httpClient.executor().orElseGet(SimpleAsyncTaskExecutor::new);
61+
}
62+
63+
/**
64+
* Create a new instance of the {@code JdkClientHttpRequestFactory} based on
65+
* the given {@link HttpClient} and {@link Executor}.
66+
* @param httpClient the client to base on
67+
* @param executor the executor to use for blocking write operations
68+
*/
69+
public JdkClientHttpRequestFactory(HttpClient httpClient, Executor executor) {
70+
Assert.notNull(httpClient, "HttpClient is required");
71+
Assert.notNull(executor, "Executor must not be null");
72+
this.httpClient = httpClient;
73+
this.executor = executor;
74+
}
75+
76+
77+
@Override
78+
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
79+
return new JdkClientHttpRequest(this.httpClient, uri, httpMethod, this.executor);
80+
}
81+
82+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2023-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.net.http.HttpClient;
22+
import java.net.http.HttpResponse;
23+
import java.util.List;
24+
import java.util.Locale;
25+
import java.util.Map;
26+
27+
import org.springframework.http.HttpHeaders;
28+
import org.springframework.http.HttpStatus;
29+
import org.springframework.http.HttpStatusCode;
30+
import org.springframework.util.CollectionUtils;
31+
import org.springframework.util.LinkedCaseInsensitiveMap;
32+
import org.springframework.util.MultiValueMap;
33+
import org.springframework.util.StreamUtils;
34+
35+
/**
36+
* {@link ClientHttpResponse} implementation based on the Java {@link HttpClient}.
37+
*
38+
* @author Marten Deinum
39+
* @author Arjen Poutsma
40+
* @since 6.1
41+
*/
42+
class JdkClientHttpResponse implements ClientHttpResponse {
43+
44+
private final HttpResponse<InputStream> response;
45+
46+
private final HttpHeaders headers;
47+
48+
private final InputStream body;
49+
50+
51+
public JdkClientHttpResponse(HttpResponse<InputStream> response) {
52+
this.response = response;
53+
this.headers = adaptHeaders(response);
54+
InputStream inputStream = response.body();
55+
this.body = (inputStream != null) ? inputStream : InputStream.nullInputStream();
56+
}
57+
58+
private static HttpHeaders adaptHeaders(HttpResponse<?> response) {
59+
Map<String, List<String>> rawHeaders = response.headers().map();
60+
Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(rawHeaders.size(), Locale.ENGLISH);
61+
MultiValueMap<String, String> multiValueMap = CollectionUtils.toMultiValueMap(map);
62+
multiValueMap.putAll(rawHeaders);
63+
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
64+
}
65+
66+
67+
@Override
68+
public HttpStatusCode getStatusCode() {
69+
return HttpStatusCode.valueOf(this.response.statusCode());
70+
}
71+
72+
@Override
73+
public String getStatusText() {
74+
// HttpResponse does not expose status text
75+
if (getStatusCode() instanceof HttpStatus status) {
76+
return status.getReasonPhrase();
77+
}
78+
else {
79+
return "";
80+
}
81+
}
82+
83+
@Override
84+
public HttpHeaders getHeaders() {
85+
return this.headers;
86+
}
87+
88+
@Override
89+
public InputStream getBody() throws IOException {
90+
return this.body;
91+
}
92+
93+
@Override
94+
public void close() {
95+
try {
96+
try {
97+
StreamUtils.drain(this.body);
98+
}
99+
finally {
100+
this.body.close();
101+
}
102+
}
103+
catch (IOException ignored) {
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)