Skip to content

Commit b3b50f8

Browse files
committed
Refactoring in the JDK HttpClient support
See gh-23432
1 parent dcc7154 commit b3b50f8

File tree

3 files changed

+81
-83
lines changed

3 files changed

+81
-83
lines changed

spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
import java.net.URI;
2020
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.concurrent.CompletableFuture;
26+
import java.util.concurrent.Flow;
2127
import java.util.function.Function;
2228

2329
import reactor.core.publisher.Mono;
@@ -27,9 +33,10 @@
2733
import org.springframework.http.HttpMethod;
2834

2935
/**
30-
* {@link ClientHttpConnector} for Java's {@link HttpClient}.
36+
* {@link ClientHttpConnector} for the Java {@link HttpClient}.
3137
*
3238
* @author Julien Eyraud
39+
* @author Rossen Stoyanchev
3340
* @since 6.0
3441
* @see <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html">HttpClient</a>
3542
*/
@@ -60,8 +67,17 @@ public JdkClientHttpConnector(HttpClient httpClient, DataBufferFactory bufferFac
6067
public Mono<ClientHttpResponse> connect(
6168
HttpMethod method, URI uri, Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
6269

63-
JdkClientHttpRequest request = new JdkClientHttpRequest(this.httpClient, method, uri, this.bufferFactory);
64-
return requestCallback.apply(request).then(Mono.defer(request::getResponse));
70+
JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory);
71+
72+
return requestCallback.apply(jdkClientHttpRequest).then(Mono.defer(() -> {
73+
HttpRequest httpRequest = jdkClientHttpRequest.getNativeRequest();
74+
75+
CompletableFuture<HttpResponse<Flow.Publisher<List<ByteBuffer>>>> future =
76+
this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher());
77+
78+
return Mono.fromCompletionStage(future)
79+
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory));
80+
}));
6581
}
6682

6783
}

spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java

Lines changed: 29 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@
1919
import java.net.URI;
2020
import java.net.http.HttpClient;
2121
import java.net.http.HttpRequest;
22-
import java.net.http.HttpResponse;
2322
import java.nio.ByteBuffer;
2423
import java.util.List;
2524
import java.util.Map;
26-
import java.util.Set;
2725
import java.util.concurrent.Flow;
2826
import java.util.function.Function;
2927
import java.util.stream.Collectors;
@@ -35,50 +33,38 @@
3533

3634
import org.springframework.core.io.buffer.DataBuffer;
3735
import org.springframework.core.io.buffer.DataBufferFactory;
36+
import org.springframework.http.HttpCookie;
3837
import org.springframework.http.HttpHeaders;
3938
import org.springframework.http.HttpMethod;
40-
import org.springframework.lang.Nullable;
4139
import org.springframework.util.Assert;
4240

4341
/**
44-
* {@link ClientHttpRequest} implementation for Java's {@link HttpClient}.
42+
* {@link ClientHttpRequest} for the Java {@link HttpClient}.
4543
*
4644
* @author Julien Eyraud
45+
* @author Rossen Stoyanchev
4746
* @since 6.0
4847
*/
4948
class JdkClientHttpRequest extends AbstractClientHttpRequest {
5049

51-
private static final Set<String> DISALLOWED_HEADERS =
52-
Set.of("connection", "content-length", "date", "expect", "from", "host", "upgrade", "via", "warning");
53-
54-
55-
private final HttpClient httpClient;
56-
5750
private final HttpMethod method;
5851

5952
private final URI uri;
6053

61-
private final HttpRequest.Builder builder;
62-
6354
private final DataBufferFactory bufferFactory;
6455

65-
@Nullable
66-
private Mono<ClientHttpResponse> response;
67-
56+
private final HttpRequest.Builder builder;
6857

69-
public JdkClientHttpRequest(
70-
HttpClient httpClient, HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory) {
7158

72-
Assert.notNull(httpClient, "HttpClient should not be null");
73-
Assert.notNull(httpMethod, "HttpMethod should not be null");
74-
Assert.notNull(uri, "URI should not be null");
75-
Assert.notNull(bufferFactory, "DataBufferFactory should not be null");
59+
public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory) {
60+
Assert.notNull(httpMethod, "HttpMethod is required");
61+
Assert.notNull(uri, "URI is required");
62+
Assert.notNull(bufferFactory, "DataBufferFactory is required");
7663

77-
this.httpClient = httpClient;
7864
this.method = httpMethod;
7965
this.uri = uri;
80-
this.builder = HttpRequest.newBuilder(uri);
8166
this.bufferFactory = bufferFactory;
67+
this.builder = HttpRequest.newBuilder(uri);
8268
}
8369

8470

@@ -103,20 +89,16 @@ public <T> T getNativeRequest() {
10389
return (T) this.builder.build();
10490
}
10591

106-
Mono<ClientHttpResponse> getResponse() {
107-
Assert.notNull(this.response, "Response is not set");
108-
return this.response;
109-
}
110-
11192

11293
@Override
11394
protected void applyHeaders() {
114-
for (Map.Entry<String, List<String>> header : getHeaders().entrySet()) {
115-
if (DISALLOWED_HEADERS.contains(header.getKey().toLowerCase())) {
95+
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
96+
if (entry.getKey().equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) {
97+
// content-length is specified when writing
11698
continue;
11799
}
118-
for (String value : header.getValue()) {
119-
this.builder.header(header.getKey(), value);
100+
for (String value : entry.getValue()) {
101+
this.builder.header(entry.getKey(), value);
120102
}
121103
}
122104
if (!getHeaders().containsKey(HttpHeaders.ACCEPT)) {
@@ -126,31 +108,28 @@ protected void applyHeaders() {
126108

127109
@Override
128110
protected void applyCookies() {
129-
this.builder.header(HttpHeaders.COOKIE,
130-
getCookies().values().stream()
131-
.flatMap(List::stream)
132-
.map(cookie -> cookie.getName() + "=" + cookie.getValue())
133-
.collect(Collectors.joining("; ")));
111+
this.builder.header(HttpHeaders.COOKIE, getCookies().values().stream()
112+
.flatMap(List::stream).map(HttpCookie::toString).collect(Collectors.joining(";")));
134113
}
135114

136115
@Override
137116
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
138117
return doCommit(() -> {
139-
Flow.Publisher<ByteBuffer> flow =
140-
JdkFlowAdapter.publisherToFlowPublisher(Flux.from(body).map(DataBuffer::asByteBuffer));
118+
this.builder.method(this.method.name(), toBodyPublisher(body));
119+
return Mono.empty();
120+
});
121+
}
141122

142-
HttpRequest.BodyPublisher bodyPublisher = (getHeaders().getContentLength() >= 0 ?
143-
HttpRequest.BodyPublishers.fromPublisher(flow, getHeaders().getContentLength()) :
144-
HttpRequest.BodyPublishers.fromPublisher(flow));
123+
private HttpRequest.BodyPublisher toBodyPublisher(Publisher<? extends DataBuffer> body) {
124+
Publisher<ByteBuffer> byteBufferBody = (body instanceof Mono ?
125+
Mono.from(body).map(DataBuffer::asByteBuffer) :
126+
Flux.from(body).map(DataBuffer::asByteBuffer));
145127

146-
this.response = Mono.fromCompletionStage(() -> {
147-
HttpRequest request = this.builder.method(this.method.name(), bodyPublisher).build();
148-
return this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher());
149-
})
150-
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory));
128+
Flow.Publisher<ByteBuffer> bodyFlow = JdkFlowAdapter.publisherToFlowPublisher(byteBufferBody);
151129

152-
return Mono.empty();
153-
});
130+
return (getHeaders().getContentLength() > 0 ?
131+
HttpRequest.BodyPublishers.fromPublisher(bodyFlow, getHeaders().getContentLength()) :
132+
HttpRequest.BodyPublishers.fromPublisher(bodyFlow));
154133
}
155134

156135
@Override
@@ -160,18 +139,8 @@ public Mono<Void> writeAndFlushWith(final Publisher<? extends Publisher<? extend
160139

161140
@Override
162141
public Mono<Void> setComplete() {
163-
if (isCommitted()) {
164-
return Mono.empty();
165-
}
166-
167142
return doCommit(() -> {
168-
this.response = Mono.fromCompletionStage(() -> {
169-
HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.noBody();
170-
HttpRequest request = this.builder.method(this.method.name(), bodyPublisher).build();
171-
return this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher());
172-
})
173-
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory));
174-
143+
this.builder.method(this.method.name(), HttpRequest.BodyPublishers.noBody());
175144
return Mono.empty();
176145
});
177146
}

spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.net.http.HttpResponse;
2222
import java.nio.ByteBuffer;
2323
import java.util.List;
24+
import java.util.Locale;
25+
import java.util.Map;
2426
import java.util.concurrent.Flow;
2527
import java.util.function.Function;
2628
import java.util.regex.Matcher;
@@ -36,13 +38,16 @@
3638
import org.springframework.http.HttpStatus;
3739
import org.springframework.http.ResponseCookie;
3840
import org.springframework.lang.Nullable;
41+
import org.springframework.util.CollectionUtils;
42+
import org.springframework.util.LinkedCaseInsensitiveMap;
3943
import org.springframework.util.LinkedMultiValueMap;
4044
import org.springframework.util.MultiValueMap;
4145

4246
/**
43-
* {@link ClientHttpResponse} implementation for Java's {@link HttpClient}.
47+
* {@link ClientHttpResponse} for the Java {@link HttpClient}.
4448
*
4549
* @author Julien Eyraud
50+
* @author Rossen Stoyanchev
4651
* @since 6.0
4752
*/
4853
class JdkClientHttpResponse implements ClientHttpResponse {
@@ -54,12 +59,23 @@ class JdkClientHttpResponse implements ClientHttpResponse {
5459

5560
private final DataBufferFactory bufferFactory;
5661

62+
private final HttpHeaders headers;
63+
5764

5865
public JdkClientHttpResponse(
5966
HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, DataBufferFactory bufferFactory) {
6067

6168
this.response = response;
6269
this.bufferFactory = bufferFactory;
70+
this.headers = adaptHeaders(response);
71+
}
72+
73+
private static HttpHeaders adaptHeaders(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response) {
74+
Map<String, List<String>> rawHeaders = response.headers().map();
75+
Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(rawHeaders.size(), Locale.ENGLISH);
76+
MultiValueMap<String, String> multiValueMap = CollectionUtils.toMultiValueMap(map);
77+
multiValueMap.putAll(rawHeaders);
78+
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
6379
}
6480

6581

@@ -75,34 +91,31 @@ public int getRawStatusCode() {
7591

7692
@Override
7793
public HttpHeaders getHeaders() {
78-
return this.response.headers().map().entrySet().stream()
79-
.collect(HttpHeaders::new,
80-
(headers, entry) -> headers.addAll(entry.getKey(), entry.getValue()),
81-
HttpHeaders::addAll);
94+
return this.headers;
8295
}
8396

8497
@Override
8598
public MultiValueMap<String, ResponseCookie> getCookies() {
8699
return this.response.headers().allValues(HttpHeaders.SET_COOKIE).stream()
87-
.flatMap(header ->
88-
HttpCookie.parse(header).stream().map(cookie ->
89-
ResponseCookie.from(cookie.getName(), cookie.getValue())
90-
.domain(cookie.getDomain())
91-
.httpOnly(cookie.isHttpOnly())
92-
.maxAge(cookie.getMaxAge())
93-
.path(cookie.getPath())
94-
.secure(cookie.getSecure())
95-
.sameSite(parseSameSite(header))
96-
.build()))
100+
.flatMap(header -> {
101+
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
102+
String sameSite = (matcher.matches() ? matcher.group(1) : null);
103+
return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite));
104+
})
97105
.collect(LinkedMultiValueMap::new,
98-
(valueMap, cookie) -> valueMap.add(cookie.getName(), cookie),
106+
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
99107
LinkedMultiValueMap::addAll);
100108
}
101109

102-
@Nullable
103-
private static String parseSameSite(String headerValue) {
104-
Matcher matcher = SAME_SITE_PATTERN.matcher(headerValue);
105-
return (matcher.matches() ? matcher.group(1) : null);
110+
private ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) {
111+
return ResponseCookie.from(cookie.getName(), cookie.getValue())
112+
.domain(cookie.getDomain())
113+
.httpOnly(cookie.isHttpOnly())
114+
.maxAge(cookie.getMaxAge())
115+
.path(cookie.getPath())
116+
.secure(cookie.getSecure())
117+
.sameSite(sameSite)
118+
.build();
106119
}
107120

108121
@Override

0 commit comments

Comments
 (0)