Skip to content

Commit 00bd0ef

Browse files
committed
Merge branch '3.5.x'
Closes gh-47946
2 parents 2f5e9b5 + 0ca8f6d commit 00bd0ef

File tree

6 files changed

+119
-39
lines changed

6 files changed

+119
-39
lines changed

module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,20 @@
1616

1717
package org.springframework.boot.http.client;
1818

19-
import java.time.Duration;
2019
import java.util.Collection;
2120
import java.util.List;
2221
import java.util.function.Consumer;
2322
import java.util.function.UnaryOperator;
2423

2524
import org.apache.hc.client5.http.classic.HttpClient;
25+
import org.apache.hc.client5.http.config.ConnectionConfig;
2626
import org.apache.hc.client5.http.config.RequestConfig;
2727
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
2828
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
2929
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
3030
import org.apache.hc.core5.http.io.SocketConfig;
3131
import org.jspecify.annotations.Nullable;
3232

33-
import org.springframework.boot.context.properties.PropertyMapper;
3433
import org.springframework.boot.http.client.HttpComponentsHttpClientBuilder.TlsSocketStrategyFactory;
3534
import org.springframework.boot.ssl.SslBundle;
3635
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
@@ -115,6 +114,20 @@ public HttpComponentsClientHttpRequestFactoryBuilder withSocketConfigCustomizer(
115114
this.httpClientBuilder.withSocketConfigCustomizer(socketConfigCustomizer));
116115
}
117116

117+
/**
118+
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
119+
* customization to the underlying
120+
* {@link org.apache.hc.client5.http.config.ConnectionConfig.Builder}.
121+
* @param connectionConfigCustomizer the customizer to apply
122+
* @return a new {@link HttpComponentsHttpClientBuilder} instance
123+
*/
124+
public HttpComponentsClientHttpRequestFactoryBuilder withConnectionConfigCustomizer(
125+
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer) {
126+
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
127+
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
128+
this.httpClientBuilder.withConnectionConfigCustomizer(connectionConfigCustomizer));
129+
}
130+
118131
/**
119132
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} with a
120133
* replacement {@link TlsSocketStrategy} factory.
@@ -158,11 +171,8 @@ public HttpComponentsClientHttpRequestFactoryBuilder with(
158171

159172
@Override
160173
protected HttpComponentsClientHttpRequestFactory createClientHttpRequestFactory(HttpClientSettings settings) {
161-
HttpClient httpClient = this.httpClientBuilder.build(settings.withConnectTimeout(null));
162-
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
163-
PropertyMapper map = PropertyMapper.get();
164-
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(factory::setConnectTimeout);
165-
return factory;
174+
HttpClient httpClient = this.httpClientBuilder.build(settings);
175+
return new HttpComponentsClientHttpRequestFactory(httpClient);
166176
}
167177

168178
static class Classes {

module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/HttpComponentsHttpClientBuilder.java

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.concurrent.TimeUnit;
2222
import java.util.function.Consumer;
2323

24+
import org.apache.hc.client5.http.config.ConnectionConfig;
2425
import org.apache.hc.client5.http.config.RequestConfig;
2526
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
2627
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -52,23 +53,27 @@ public final class HttpComponentsHttpClientBuilder {
5253

5354
private final Consumer<SocketConfig.Builder> socketConfigCustomizer;
5455

56+
private final Consumer<ConnectionConfig.Builder> connectionConfigCustomizer;
57+
5558
private final Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer;
5659

5760
private final TlsSocketStrategyFactory tlsSocketStrategyFactory;
5861

5962
public HttpComponentsHttpClientBuilder() {
60-
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
63+
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
6164
HttpComponentsSslBundleTlsStrategy::get);
6265
}
6366

6467
private HttpComponentsHttpClientBuilder(Consumer<HttpClientBuilder> customizer,
6568
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer,
6669
Consumer<SocketConfig.Builder> socketConfigCustomizer,
70+
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer,
6771
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
6872
TlsSocketStrategyFactory tlsSocketStrategyFactory) {
6973
this.customizer = customizer;
7074
this.connectionManagerCustomizer = connectionManagerCustomizer;
7175
this.socketConfigCustomizer = socketConfigCustomizer;
76+
this.connectionConfigCustomizer = connectionConfigCustomizer;
7277
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
7378
this.tlsSocketStrategyFactory = tlsSocketStrategyFactory;
7479
}
@@ -82,8 +87,8 @@ private HttpComponentsHttpClientBuilder(Consumer<HttpClientBuilder> customizer,
8287
public HttpComponentsHttpClientBuilder withCustomizer(Consumer<HttpClientBuilder> customizer) {
8388
Assert.notNull(customizer, "'customizer' must not be null");
8489
return new HttpComponentsHttpClientBuilder(this.customizer.andThen(customizer),
85-
this.connectionManagerCustomizer, this.socketConfigCustomizer, this.defaultRequestConfigCustomizer,
86-
this.tlsSocketStrategyFactory);
90+
this.connectionManagerCustomizer, this.socketConfigCustomizer, this.connectionConfigCustomizer,
91+
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
8792
}
8893

8994
/**
@@ -97,7 +102,7 @@ public HttpComponentsHttpClientBuilder withConnectionManagerCustomizer(
97102
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
98103
return new HttpComponentsHttpClientBuilder(this.customizer,
99104
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.socketConfigCustomizer,
100-
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
105+
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
101106
}
102107

103108
/**
@@ -111,8 +116,23 @@ public HttpComponentsHttpClientBuilder withSocketConfigCustomizer(
111116
Consumer<SocketConfig.Builder> socketConfigCustomizer) {
112117
Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null");
113118
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
114-
this.socketConfigCustomizer.andThen(socketConfigCustomizer), this.defaultRequestConfigCustomizer,
115-
this.tlsSocketStrategyFactory);
119+
this.socketConfigCustomizer.andThen(socketConfigCustomizer), this.connectionConfigCustomizer,
120+
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
121+
}
122+
123+
/**
124+
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
125+
* customization to the underlying
126+
* {@link org.apache.hc.client5.http.config.ConnectionConfig.Builder}.
127+
* @param connectionConfigCustomizer the customizer to apply
128+
* @return a new {@link HttpComponentsHttpClientBuilder} instance
129+
*/
130+
public HttpComponentsHttpClientBuilder withConnectionConfigCustomizer(
131+
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer) {
132+
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
133+
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
134+
this.socketConfigCustomizer.andThen(this.socketConfigCustomizer), this.connectionConfigCustomizer,
135+
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
116136
}
117137

118138
/**
@@ -128,7 +148,8 @@ public HttpComponentsHttpClientBuilder withTlsSocketStrategyFactory(
128148
TlsSocketStrategyFactory tlsSocketStrategyFactory) {
129149
Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null");
130150
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
131-
this.socketConfigCustomizer, this.defaultRequestConfigCustomizer, tlsSocketStrategyFactory);
151+
this.socketConfigCustomizer, this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer,
152+
tlsSocketStrategyFactory);
132153
}
133154

134155
/**
@@ -143,7 +164,7 @@ public HttpComponentsHttpClientBuilder withDefaultRequestConfigCustomizer(
143164
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
144165
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
145166
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
146-
this.socketConfigCustomizer,
167+
this.socketConfigCustomizer, this.connectionConfigCustomizer,
147168
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer),
148169
this.tlsSocketStrategyFactory);
149170
}
@@ -155,7 +176,6 @@ public HttpComponentsHttpClientBuilder withDefaultRequestConfigCustomizer(
155176
*/
156177
public CloseableHttpClient build(@Nullable HttpClientSettings settings) {
157178
settings = (settings != null) ? settings : HttpClientSettings.defaults();
158-
Assert.isTrue(settings.connectTimeout() == null, "'settings' must not have a 'connectTimeout'");
159179
HttpClientBuilder builder = HttpClientBuilder.create()
160180
.useSystemProperties()
161181
.setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects()))
@@ -169,7 +189,8 @@ private PoolingHttpClientConnectionManager createConnectionManager(HttpClientSet
169189
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create()
170190
.useSystemProperties();
171191
PropertyMapper map = PropertyMapper.get();
172-
builder.setDefaultSocketConfig(createSocketConfig(settings));
192+
builder.setDefaultSocketConfig(createSocketConfig());
193+
builder.setDefaultConnectionConfig(createConnectionConfig(settings));
173194
map.from(settings::sslBundle)
174195
.always()
175196
.as(this.tlsSocketStrategyFactory::getTlsSocketStrategy)
@@ -178,13 +199,22 @@ private PoolingHttpClientConnectionManager createConnectionManager(HttpClientSet
178199
return builder.build();
179200
}
180201

181-
private SocketConfig createSocketConfig(HttpClientSettings settings) {
202+
private SocketConfig createSocketConfig() {
182203
SocketConfig.Builder builder = SocketConfig.custom();
204+
this.socketConfigCustomizer.accept(builder);
205+
return builder.build();
206+
}
207+
208+
private ConnectionConfig createConnectionConfig(HttpClientSettings settings) {
209+
ConnectionConfig.Builder builder = ConnectionConfig.custom();
183210
PropertyMapper map = PropertyMapper.get();
211+
map.from(settings::connectTimeout)
212+
.as(Duration::toMillis)
213+
.to((timeout) -> builder.setConnectTimeout(timeout, TimeUnit.MILLISECONDS));
184214
map.from(settings::readTimeout)
185215
.asInt(Duration::toMillis)
186-
.to((timeout) -> builder.setSoTimeout(timeout, TimeUnit.MILLISECONDS));
187-
this.socketConfigCustomizer.accept(builder);
216+
.to((timeout) -> builder.setSocketTimeout(timeout, TimeUnit.MILLISECONDS));
217+
this.connectionConfigCustomizer.accept(builder);
188218
return builder.build();
189219
}
190220

module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
import java.util.List;
2121

2222
import org.apache.hc.client5.http.HttpRoute;
23-
import org.apache.hc.client5.http.classic.HttpClient;
23+
import org.apache.hc.client5.http.config.ConnectionConfig;
2424
import org.apache.hc.client5.http.config.RequestConfig;
25+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
2526
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
2627
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
2728
import org.apache.hc.core5.function.Resolver;
@@ -103,22 +104,25 @@ void with() {
103104

104105
@Override
105106
protected long connectTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
106-
Object field = ReflectionTestUtils.getField(requestFactory, "connectTimeout");
107-
assertThat(field).isNotNull();
108-
return (long) field;
107+
return getConnectorConfig(requestFactory).getConnectTimeout().toMilliseconds();
109108
}
110109

111110
@Override
112-
@SuppressWarnings("unchecked")
113111
protected long readTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
114-
HttpClient httpClient = requestFactory.getHttpClient();
115-
Object connectionManager = ReflectionTestUtils.getField(httpClient, "connManager");
116-
assertThat(connectionManager).isNotNull();
117-
Resolver<HttpRoute, SocketConfig> socketConfigResolver = (Resolver<HttpRoute, SocketConfig>) ReflectionTestUtils
118-
.getField(connectionManager, "socketConfigResolver");
119-
assertThat(socketConfigResolver).isNotNull();
120-
SocketConfig socketConfig = socketConfigResolver.resolve(null);
121-
return socketConfig.getSoTimeout().toMilliseconds();
112+
return getConnectorConfig(requestFactory).getSocketTimeout().toMilliseconds();
113+
}
114+
115+
@SuppressWarnings("unchecked")
116+
private ConnectionConfig getConnectorConfig(HttpComponentsClientHttpRequestFactory requestFactory) {
117+
CloseableHttpClient httpClient = (CloseableHttpClient) ReflectionTestUtils.getField(requestFactory,
118+
"httpClient");
119+
assertThat(httpClient).isNotNull();
120+
Object manager = ReflectionTestUtils.getField(httpClient, "connManager");
121+
assertThat(manager).isNotNull();
122+
Resolver<HttpRoute, ConnectionConfig> resolver = (Resolver<HttpRoute, ConnectionConfig>) ReflectionTestUtils
123+
.getField(manager, "connectionConfigResolver");
124+
assertThat(resolver).isNotNull();
125+
return resolver.resolve(null);
122126
}
123127

124128
}

module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/imperative/ImperativeHttpClientAutoConfigurationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ void clientHttpRequestFactoryBuilderCustomizersAreApplied() {
138138
this.contextRunner.withUserConfiguration(ClientHttpRequestFactoryBuilderCustomizersConfiguration.class)
139139
.run((context) -> {
140140
ClientHttpRequestFactory factory = context.getBean(ClientHttpRequestFactoryBuilder.class).build();
141-
assertThat(factory).extracting("connectTimeout").isEqualTo(5L);
141+
assertThat(factory).extracting("readTimeout").isEqualTo(5L);
142142
});
143143
}
144144

@@ -160,7 +160,7 @@ static class ClientHttpRequestFactoryBuilderCustomizersConfiguration {
160160

161161
@Bean
162162
ClientHttpRequestFactoryBuilderCustomizer<HttpComponentsClientHttpRequestFactoryBuilder> httpComponentsCustomizer() {
163-
return (builder) -> builder.withCustomizer((factory) -> factory.setConnectTimeout(5));
163+
return (builder) -> builder.withCustomizer((factory) -> factory.setReadTimeout(5));
164164
}
165165

166166
@Bean

module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/RestTemplateBuilderTests.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
import java.util.Set;
2525
import java.util.function.Supplier;
2626

27+
import org.apache.hc.client5.http.HttpRoute;
28+
import org.apache.hc.client5.http.config.ConnectionConfig;
29+
import org.apache.hc.core5.function.Resolver;
30+
import org.apache.hc.core5.util.Timeout;
31+
import org.assertj.core.extractor.Extractors;
2732
import org.junit.jupiter.api.Test;
2833
import org.junit.jupiter.api.extension.ExtendWith;
2934
import org.mockito.InOrder;
@@ -354,10 +359,18 @@ void defaultHeaderWhenUsingMockRestServiceServerAddsHeader() {
354359
}
355360

356361
@Test
362+
@SuppressWarnings("unchecked")
357363
void clientSettingsAppliesSettings() {
358-
HttpClientSettings settings = HttpClientSettings.defaults().withConnectTimeout(Duration.ofSeconds(1));
364+
HttpClientSettings settings = HttpClientSettings.defaults()
365+
.withConnectTimeout(Duration.ofSeconds(1))
366+
.withReadTimeout(Duration.ofSeconds(2));
359367
RestTemplate template = this.builder.clientSettings(settings).build();
360-
assertThat(template.getRequestFactory()).extracting("connectTimeout").isEqualTo(1000L);
368+
Resolver<HttpRoute, ConnectionConfig> resolver = (Resolver<HttpRoute, ConnectionConfig>) Extractors
369+
.byName("httpClient.connManager.connectionConfigResolver")
370+
.apply(template.getRequestFactory());
371+
ConnectionConfig config = resolver.resolve(mock());
372+
assertThat(config.getConnectTimeout()).isEqualTo(Timeout.of(Duration.ofSeconds(1)));
373+
assertThat(config.getSocketTimeout()).isEqualTo(Timeout.of(Duration.ofSeconds(2)));
361374
}
362375

363376
@Test

module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfigurationTests.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020
import java.lang.reflect.Proxy;
2121
import java.net.http.HttpClient;
2222
import java.net.http.HttpClient.Redirect;
23+
import java.time.Duration;
2324
import java.util.HashMap;
2425
import java.util.Map;
2526

27+
import org.apache.hc.client5.http.HttpRoute;
28+
import org.apache.hc.client5.http.config.ConnectionConfig;
29+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
30+
import org.apache.hc.core5.function.Resolver;
31+
import org.apache.hc.core5.util.Timeout;
2632
import org.assertj.core.extractor.Extractors;
2733
import org.junit.jupiter.api.Test;
2834
import org.mockito.ArgumentCaptor;
@@ -40,6 +46,8 @@
4046
import org.springframework.context.annotation.Bean;
4147
import org.springframework.context.annotation.Configuration;
4248
import org.springframework.http.client.ClientHttpRequestFactory;
49+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
50+
import org.springframework.test.util.ReflectionTestUtils;
4351
import org.springframework.test.web.client.MockRestServiceServer;
4452
import org.springframework.web.client.RestClient;
4553
import org.springframework.web.client.RestClient.Builder;
@@ -128,8 +136,23 @@ private void assertConnectTimeout(ClientCallback<RestClient.Builder> callback, S
128136
callback.withClient(group, builder);
129137
ArgumentCaptor<ClientHttpRequestFactory> requestFactoryCaptor = ArgumentCaptor.captor();
130138
then(builder).should().requestFactory(requestFactoryCaptor.capture());
131-
ClientHttpRequestFactory client = requestFactoryCaptor.getValue();
132-
assertThat(client).extracting("connectTimeout").isEqualTo(expectedReadTimeout);
139+
HttpComponentsClientHttpRequestFactory client = (HttpComponentsClientHttpRequestFactory) requestFactoryCaptor
140+
.getValue();
141+
assertThat(getConnectorConfig(client).getConnectTimeout())
142+
.isEqualTo(Timeout.of(Duration.ofMillis(expectedReadTimeout)));
143+
}
144+
145+
@SuppressWarnings("unchecked")
146+
private ConnectionConfig getConnectorConfig(HttpComponentsClientHttpRequestFactory requestFactory) {
147+
CloseableHttpClient httpClient = (CloseableHttpClient) ReflectionTestUtils.getField(requestFactory,
148+
"httpClient");
149+
assertThat(httpClient).isNotNull();
150+
Object manager = ReflectionTestUtils.getField(httpClient, "connManager");
151+
assertThat(manager).isNotNull();
152+
Resolver<HttpRoute, ConnectionConfig> resolver = (Resolver<HttpRoute, ConnectionConfig>) ReflectionTestUtils
153+
.getField(manager, "connectionConfigResolver");
154+
assertThat(resolver).isNotNull();
155+
return resolver.resolve(null);
133156
}
134157

135158
@Test

0 commit comments

Comments
 (0)