Skip to content

Commit 2ca8dd2

Browse files
mdeinumpoutsma
authored andcommitted
HttpClient based ClientHttpRequestFactory
As JDK17 is now the baseline it is possible to use the HttpClient provided by Java.
1 parent c2e3fed commit 2ca8dd2

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.URI;
22+
import java.net.http.HttpClient;
23+
import java.net.http.HttpRequest;
24+
import java.net.http.HttpResponse;
25+
import java.util.List;
26+
27+
import org.springframework.http.HttpHeaders;
28+
import org.springframework.http.HttpMethod;
29+
30+
/**
31+
* {@link ClientHttpRequest} implementation based on the Java {@code HttpClient}.
32+
*
33+
* @author Marten Deinum
34+
* @since 6.1
35+
*/
36+
public class JdkClientClientHttpRequest extends AbstractBufferingClientHttpRequest {
37+
38+
/*
39+
* The JDK HttpRequest doesn't allow all headers to be set. The named headers are taken from the default
40+
* implementation for HttpRequest.
41+
*/
42+
private static final List<String> DISALLOWED_HEADERS =
43+
List.of("connection", "content-length", "expect", "host", "upgrade");
44+
45+
private final HttpClient client;
46+
private final URI uri;
47+
private final HttpMethod method;
48+
public JdkClientClientHttpRequest(HttpClient client, URI uri, HttpMethod method) {
49+
this.client = client;
50+
this.uri = uri;
51+
this.method = method;
52+
}
53+
54+
@Override
55+
public HttpMethod getMethod() {
56+
return this.method;
57+
}
58+
59+
@Override
60+
public URI getURI() {
61+
return this.uri;
62+
}
63+
64+
@Override
65+
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException {
66+
67+
HttpRequest.Builder builder = HttpRequest.newBuilder(this.uri)
68+
.method(getMethod().name(), HttpRequest.BodyPublishers.ofByteArray(content));
69+
70+
addHeaders(headers, builder);
71+
HttpRequest request = builder.build();
72+
HttpResponse<InputStream> response;
73+
try {
74+
response = this.client.send(request, HttpResponse.BodyHandlers.ofInputStream());
75+
} catch (InterruptedException ex)
76+
{
77+
Thread.currentThread().interrupt();
78+
throw new IllegalStateException("Request interupted.", ex);
79+
}
80+
return new JdkClientClientHttpResponse(response);
81+
}
82+
83+
private static void addHeaders(HttpHeaders headers, HttpRequest.Builder builder) {
84+
headers.forEach((headerName, headerValues) -> {
85+
if (!DISALLOWED_HEADERS.contains(headerName.toLowerCase())) {
86+
for (String headerValue : headerValues) {
87+
builder.header(headerName, headerValue);
88+
}
89+
}
90+
});
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
23+
import org.springframework.http.HttpMethod;
24+
25+
26+
/**
27+
* {@link ClientHttpRequestFactory} implementation that uses a
28+
* <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/HttpClient.html">HttpClient</a> to create requests.
29+
*
30+
* @author Marten Deinum
31+
* @since 6.1
32+
*/
33+
public class JdkClientClientHttpRequestFactory implements ClientHttpRequestFactory {
34+
35+
private HttpClient client;
36+
37+
private final boolean defaultClient;
38+
39+
40+
public JdkClientClientHttpRequestFactory() {
41+
this.client = HttpClient.newHttpClient();
42+
this.defaultClient = true;
43+
}
44+
45+
public JdkClientClientHttpRequestFactory(HttpClient client) {
46+
this.client = client;
47+
this.defaultClient = false;
48+
}
49+
50+
@Override
51+
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
52+
return new JdkClientClientHttpRequest(this.client, uri, httpMethod);
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.HttpResponse;
22+
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpStatus;
25+
import org.springframework.http.HttpStatusCode;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.StreamUtils;
28+
29+
/**
30+
* {@link ClientHttpResponse} implementation based on the Java {@code HttpClient}.
31+
*
32+
* @author Marten Deinum
33+
* @since 6.1
34+
*/
35+
public class JdkClientClientHttpResponse implements ClientHttpResponse {
36+
37+
private final HttpResponse<InputStream> response;
38+
@Nullable
39+
private volatile HttpHeaders headers;
40+
41+
public JdkClientClientHttpResponse(HttpResponse<InputStream> response) {
42+
this.response = response;
43+
}
44+
45+
@Override
46+
public HttpStatusCode getStatusCode() throws IOException {
47+
return HttpStatusCode.valueOf(this.response.statusCode());
48+
}
49+
50+
@Override
51+
@Deprecated
52+
public int getRawStatusCode() {
53+
return this.response.statusCode();
54+
}
55+
56+
@Override
57+
public String getStatusText() {
58+
HttpStatus status = HttpStatus.resolve(this.response.statusCode());
59+
return (status != null) ? status.getReasonPhrase() : "";
60+
}
61+
62+
@Override
63+
public InputStream getBody() throws IOException {
64+
InputStream body = this.response.body();
65+
return (body != null ? body : InputStream.nullInputStream());
66+
}
67+
68+
@Override
69+
public HttpHeaders getHeaders() {
70+
HttpHeaders headers = this.headers;
71+
if (headers == null) {
72+
headers = new HttpHeaders();
73+
for (String headerName : this.response.headers().map().keySet()) {
74+
for (String headerValue : this.response.headers().allValues(headerName)) {
75+
headers.add(headerName, headerValue);
76+
}
77+
}
78+
this.headers = headers;
79+
}
80+
return headers;
81+
}
82+
83+
@Override
84+
public void close() {
85+
InputStream body = this.response.body();
86+
try {
87+
try {
88+
StreamUtils.drain(body);
89+
}
90+
finally {
91+
body.close();
92+
}
93+
}
94+
catch (IOException ex) {
95+
// Ignore exception on close...
96+
}
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 org.junit.jupiter.api.Test;
20+
21+
import org.springframework.http.HttpMethod;
22+
23+
/**
24+
* @author Marten Deinum
25+
*/
26+
public class JdkClientClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
27+
28+
@Override
29+
protected ClientHttpRequestFactory createRequestFactory() {
30+
return new JdkClientClientHttpRequestFactory();
31+
}
32+
33+
@Override
34+
@Test
35+
public void httpMethods() throws Exception {
36+
super.httpMethods();
37+
assertHttpMethod("patch", HttpMethod.PATCH);
38+
}
39+
40+
}

0 commit comments

Comments
 (0)