Skip to content

Commit e379f4b

Browse files
author
Julien Eyraud
committed
JDK 11 HttpClient integration with WebClient
This a simple implementation of ClientHttpResponse that levrage JDK 11 HttpClient. Closes gh-21014
1 parent f440fb8 commit e379f4b

File tree

5 files changed

+337
-2
lines changed

5 files changed

+337
-2
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2002-2019 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.reactive;
18+
19+
import java.net.URI;
20+
import java.net.http.HttpClient;
21+
import java.util.function.Function;
22+
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.core.io.buffer.DataBufferFactory;
26+
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
27+
import org.springframework.http.HttpMethod;
28+
29+
/**
30+
* {@link ClientHttpConnector} for the Java 11 HTTP client.
31+
*
32+
* @author Julien Eyraud
33+
* @since 5.2
34+
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html">Java HttpClient</a>
35+
*/
36+
public class JdkClientHttpConnector implements ClientHttpConnector {
37+
38+
private final HttpClient httpClient;
39+
40+
private final DataBufferFactory dataBufferFactory;
41+
42+
43+
/**
44+
* Default constructor that creates a new instance of {@link HttpClient} and a {@link DataBufferFactory}.
45+
*/
46+
public JdkClientHttpConnector() {
47+
this(HttpClient.newHttpClient(), new DefaultDataBufferFactory());
48+
}
49+
50+
/**
51+
* Constructor with an initialized {@link HttpClient} and a initialized {@link DataBufferFactory}.
52+
*/
53+
public JdkClientHttpConnector(final HttpClient httpClient, final DataBufferFactory dataBufferFactory) {
54+
this.httpClient = httpClient;
55+
this.dataBufferFactory = dataBufferFactory;
56+
}
57+
58+
@Override
59+
public Mono<ClientHttpResponse> connect(final HttpMethod method, final URI uri, final Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
60+
final JdkClientHttpRequest request = new JdkClientHttpRequest(this.httpClient, method, uri, this.dataBufferFactory);
61+
return requestCallback.apply(request).then(Mono.defer(request::getResponse));
62+
}
63+
64+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2002-2019 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.reactive;
18+
19+
import java.net.URI;
20+
import java.net.http.HttpClient;
21+
import java.net.http.HttpRequest;
22+
import java.net.http.HttpResponse;
23+
import java.nio.ByteBuffer;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.concurrent.Flow;
28+
import java.util.function.Function;
29+
import java.util.stream.Collectors;
30+
31+
import org.reactivestreams.Publisher;
32+
import reactor.adapter.JdkFlowAdapter;
33+
import reactor.core.publisher.Flux;
34+
import reactor.core.publisher.Mono;
35+
36+
import org.springframework.core.io.buffer.DataBuffer;
37+
import org.springframework.core.io.buffer.DataBufferFactory;
38+
import org.springframework.http.HttpHeaders;
39+
import org.springframework.http.HttpMethod;
40+
import org.springframework.util.Assert;
41+
42+
/**
43+
* {@link ClientHttpRequest} implementation for the Java 11 HTTP client.
44+
*
45+
* @author Julien Eyraud
46+
* @since 5.2
47+
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html">Java HttpClient</a>
48+
*/
49+
class JdkClientHttpRequest extends AbstractClientHttpRequest {
50+
51+
private static final Set<String> DISALLOWED_HEADERS = Set.of("connection", "content-length", "date", "expect", "from", "host", "upgrade", "via", "warning");
52+
53+
private final HttpClient httpClient;
54+
55+
private final HttpMethod method;
56+
57+
private final URI uri;
58+
59+
private final HttpRequest.Builder builder;
60+
61+
private final DataBufferFactory bufferFactory;
62+
63+
private Mono<ClientHttpResponse> response;
64+
65+
66+
public JdkClientHttpRequest(final HttpClient httpClient, final HttpMethod httpMethod, final URI uri, final DataBufferFactory bufferFactory) {
67+
Assert.notNull(httpClient, "HttpClient should not be null");
68+
Assert.notNull(httpMethod, "HttpMethod should not be null");
69+
Assert.notNull(uri, "URI should not be null");
70+
Assert.notNull(bufferFactory, "DataBufferFactory should not be null");
71+
this.httpClient = httpClient;
72+
this.method = httpMethod;
73+
this.uri = uri;
74+
this.builder = HttpRequest.newBuilder(uri);
75+
this.bufferFactory = bufferFactory;
76+
}
77+
78+
@Override
79+
protected void applyHeaders() {
80+
HttpHeaders headers = getHeaders();
81+
for (Map.Entry<String, List<String>> header : getHeaders().entrySet()) {
82+
if (!DISALLOWED_HEADERS.contains(header.getKey().toLowerCase())) {
83+
for (String value : header.getValue()) {
84+
this.builder.header(header.getKey(), value);
85+
}
86+
}
87+
}
88+
if (!headers.containsKey(HttpHeaders.ACCEPT)) {
89+
this.builder.header(HttpHeaders.ACCEPT, "*/*");
90+
}
91+
}
92+
93+
@Override
94+
protected void applyCookies() {
95+
final String cookies = getCookies().values().stream().flatMap(List::stream).map(c -> c.getName() + "=" + c.getValue()).collect(Collectors.joining("; "));
96+
this.builder.header(HttpHeaders.COOKIE, cookies);
97+
}
98+
99+
@Override
100+
public HttpMethod getMethod() {
101+
return this.method;
102+
}
103+
104+
@Override
105+
public URI getURI() {
106+
return this.uri;
107+
}
108+
109+
@Override
110+
public DataBufferFactory bufferFactory() {
111+
return this.bufferFactory;
112+
}
113+
114+
@Override
115+
@SuppressWarnings("unchecked")
116+
public <T> T getNativeRequest() {
117+
return (T) this.builder.build();
118+
}
119+
120+
@Override
121+
public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {
122+
return doCommit(() -> {
123+
final Flow.Publisher<ByteBuffer> flowAdapter = JdkFlowAdapter.publisherToFlowPublisher(Flux.from(body).map(DataBuffer::asByteBuffer));
124+
final long contentLength = getHeaders().getContentLength();
125+
final HttpRequest.BodyPublisher bodyPublisher = contentLength >= 0 ? HttpRequest.BodyPublishers.fromPublisher(flowAdapter, contentLength)
126+
: HttpRequest.BodyPublishers.fromPublisher(flowAdapter);
127+
this.response = Mono
128+
.fromCompletionStage(() -> this.httpClient.sendAsync(this.builder.method(this.method.name(), bodyPublisher).build(), HttpResponse.BodyHandlers.ofPublisher()))
129+
.map(r -> new JdkClientHttpResponse(r, this.bufferFactory));
130+
return Mono.empty();
131+
});
132+
}
133+
134+
@Override
135+
public Mono<Void> writeAndFlushWith(final Publisher<? extends Publisher<? extends DataBuffer>> body) {
136+
return writeWith(Flux.from(body).flatMap(Function.identity()));
137+
}
138+
139+
@Override
140+
public Mono<Void> setComplete() {
141+
if (isCommitted()) {
142+
return Mono.empty();
143+
}
144+
else {
145+
return doCommit(() -> {
146+
this.response = Mono
147+
.fromCompletionStage(() -> this.httpClient.sendAsync(this.builder.method(this.method.name(), HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.ofPublisher()))
148+
.map(r -> new JdkClientHttpResponse(r, this.bufferFactory));
149+
return Mono.empty();
150+
});
151+
}
152+
}
153+
154+
public Mono<ClientHttpResponse> getResponse() {
155+
return this.response;
156+
}
157+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2002-2019 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.reactive;
18+
19+
import java.net.HttpCookie;
20+
import java.net.http.HttpResponse;
21+
import java.nio.ByteBuffer;
22+
import java.util.List;
23+
import java.util.concurrent.Flow;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
26+
27+
import reactor.adapter.JdkFlowAdapter;
28+
import reactor.core.publisher.Flux;
29+
30+
import org.springframework.core.io.buffer.DataBuffer;
31+
import org.springframework.core.io.buffer.DataBufferFactory;
32+
import org.springframework.core.io.buffer.DataBufferUtils;
33+
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpStatus;
35+
import org.springframework.http.ResponseCookie;
36+
import org.springframework.lang.Nullable;
37+
import org.springframework.util.LinkedMultiValueMap;
38+
import org.springframework.util.MultiValueMap;
39+
40+
/**
41+
* {@link ClientHttpResponse} implementation for the Java 11 HTTP client.
42+
*
43+
* @author Julien Eyraud
44+
* @since 5.2
45+
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html">Java HttpClient</a>
46+
*/
47+
class JdkClientHttpResponse implements ClientHttpResponse {
48+
private static final Pattern SAMESITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
49+
50+
private final HttpResponse<Flow.Publisher<List<ByteBuffer>>> response;
51+
52+
private final DataBufferFactory bufferFactory;
53+
54+
55+
public JdkClientHttpResponse(final HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, final DataBufferFactory bufferFactory) {
56+
this.response = response;
57+
this.bufferFactory = bufferFactory;
58+
}
59+
60+
@Nullable
61+
private static String parseSameSite(String headerValue) {
62+
Matcher matcher = SAMESITE_PATTERN.matcher(headerValue);
63+
return (matcher.matches() ? matcher.group(1) : null);
64+
}
65+
66+
@Override
67+
public HttpStatus getStatusCode() {
68+
return HttpStatus.resolve(this.response.statusCode());
69+
}
70+
71+
@Override
72+
public int getRawStatusCode() {
73+
return this.response.statusCode();
74+
}
75+
76+
@Override
77+
public MultiValueMap<String, ResponseCookie> getCookies() {
78+
return this.response.headers().allValues(HttpHeaders.SET_COOKIE).stream()
79+
.flatMap(header ->
80+
HttpCookie.parse(header).stream().map(httpCookie ->
81+
ResponseCookie
82+
.from(httpCookie.getName(), httpCookie.getValue())
83+
.domain(httpCookie.getDomain())
84+
.httpOnly(httpCookie.isHttpOnly())
85+
.maxAge(httpCookie.getMaxAge())
86+
.path(httpCookie.getPath())
87+
.secure(httpCookie.getSecure())
88+
.sameSite(parseSameSite(header))
89+
.build()
90+
)
91+
).collect(LinkedMultiValueMap::new, (m, v) -> m.add(v.getName(), v), LinkedMultiValueMap::addAll);
92+
}
93+
94+
@Override
95+
public Flux<DataBuffer> getBody() {
96+
return JdkFlowAdapter
97+
.flowPublisherToFlux(this.response.body())
98+
.flatMap(Flux::fromIterable)
99+
.map(this.bufferFactory::wrap)
100+
.doOnDiscard(DataBuffer.class, DataBufferUtils::release);
101+
}
102+
103+
@Override
104+
public HttpHeaders getHeaders() {
105+
return this.response.headers().map().entrySet().stream().collect(HttpHeaders::new, (headers, entry) -> headers.addAll(entry.getKey(), entry.getValue()), HttpHeaders::addAll);
106+
}
107+
}

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import org.springframework.http.ResponseEntity;
6363
import org.springframework.http.client.reactive.ClientHttpConnector;
6464
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
65+
import org.springframework.http.client.reactive.JdkClientHttpConnector;
6566
import org.springframework.http.client.reactive.JettyClientHttpConnector;
6667
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
6768
import org.springframework.web.reactive.function.BodyExtractors;
@@ -92,7 +93,8 @@ static Stream<ClientHttpConnector> arguments() {
9293
return Stream.of(
9394
new ReactorClientHttpConnector(),
9495
new JettyClientHttpConnector(),
95-
new HttpComponentsClientHttpConnector()
96+
new HttpComponentsClientHttpConnector(),
97+
new JdkClientHttpConnector()
9698
);
9799
}
98100

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.core.ParameterizedTypeReference;
3737
import org.springframework.http.client.reactive.ClientHttpConnector;
3838
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
39+
import org.springframework.http.client.reactive.JdkClientHttpConnector;
3940
import org.springframework.http.client.reactive.JettyClientHttpConnector;
4041
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
4142
import org.springframework.http.codec.ServerSentEvent;
@@ -76,15 +77,19 @@ static Object[][] arguments() {
7677
{new JettyHttpServer(), new ReactorClientHttpConnector()},
7778
{new JettyHttpServer(), new JettyClientHttpConnector()},
7879
{new JettyHttpServer(), new HttpComponentsClientHttpConnector()},
80+
{new JettyHttpServer(), new JdkClientHttpConnector()},
7981
{new ReactorHttpServer(), new ReactorClientHttpConnector()},
8082
{new ReactorHttpServer(), new JettyClientHttpConnector()},
8183
{new ReactorHttpServer(), new HttpComponentsClientHttpConnector()},
84+
{new ReactorHttpServer(), new JdkClientHttpConnector()},
8285
{new TomcatHttpServer(), new ReactorClientHttpConnector()},
8386
{new TomcatHttpServer(), new JettyClientHttpConnector()},
8487
{new TomcatHttpServer(), new HttpComponentsClientHttpConnector()},
88+
{new TomcatHttpServer(), new JdkClientHttpConnector()},
8589
{new UndertowHttpServer(), new ReactorClientHttpConnector()},
8690
{new UndertowHttpServer(), new JettyClientHttpConnector()},
87-
{new UndertowHttpServer(), new HttpComponentsClientHttpConnector()}
91+
{new UndertowHttpServer(), new HttpComponentsClientHttpConnector()},
92+
{new UndertowHttpServer(), new JdkClientHttpConnector()},
8893
};
8994
}
9095

0 commit comments

Comments
 (0)