Skip to content

Commit d3454c5

Browse files
committed
Improve creation of HttpClient with HttpComponents5ClientFactory
This commit improves HttpComponents5ClientFactory so that it is easier to apply custom configuration to the HttpClient it creates. Closes gh-1035
1 parent f250abb commit d3454c5

File tree

7 files changed

+200
-104
lines changed

7 files changed

+200
-104
lines changed

spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@
2222
import org.apache.hc.client5.http.classic.HttpClient;
2323
import org.apache.hc.client5.http.classic.methods.HttpPost;
2424
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
25-
import org.apache.hc.core5.http.EntityDetails;
26-
import org.apache.hc.core5.http.HttpException;
27-
import org.apache.hc.core5.http.HttpHeaders;
2825
import org.apache.hc.core5.http.HttpHost;
29-
import org.apache.hc.core5.http.HttpRequest;
30-
import org.apache.hc.core5.http.HttpRequestInterceptor;
3126
import org.apache.hc.core5.http.protocol.HttpContext;
3227

3328
import org.springframework.beans.factory.DisposableBean;
@@ -84,27 +79,4 @@ protected HttpContext createContext(URI uri) {
8479
return null;
8580
}
8681

87-
/**
88-
* HttpClient {@link HttpRequestInterceptor} implementation that removes
89-
* {@code Content-Length} and {@code Transfer-Encoding} headers from the request.
90-
* Necessary, because some SAAJ and other SOAP implementations set these headers
91-
* themselves, and HttpClient throws an exception if they have been set.
92-
*/
93-
public static class RemoveSoapHeadersInterceptor implements HttpRequestInterceptor {
94-
95-
@Override
96-
public void process(HttpRequest request, EntityDetails entityDetails, HttpContext httpContext)
97-
throws HttpException, IOException {
98-
99-
if (request.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
100-
request.removeHeaders(HttpHeaders.TRANSFER_ENCODING);
101-
}
102-
103-
if (request.containsHeader(HttpHeaders.CONTENT_LENGTH)) {
104-
request.removeHeaders(HttpHeaders.CONTENT_LENGTH);
105-
}
106-
}
107-
108-
}
109-
11082
}

spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java

Lines changed: 125 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package org.springframework.ws.transport.http;
1818

19+
import java.io.IOException;
1920
import java.net.URI;
20-
import java.net.URISyntaxException;
2121
import java.time.Duration;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import java.util.Map;
2325

2426
import org.apache.hc.client5.http.HttpRoute;
@@ -30,7 +32,13 @@
3032
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
3133
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
3234
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
35+
import org.apache.hc.core5.http.EntityDetails;
36+
import org.apache.hc.core5.http.HttpException;
37+
import org.apache.hc.core5.http.HttpHeaders;
3338
import org.apache.hc.core5.http.HttpHost;
39+
import org.apache.hc.core5.http.HttpRequest;
40+
import org.apache.hc.core5.http.HttpRequestInterceptor;
41+
import org.apache.hc.core5.http.protocol.HttpContext;
3442
import org.apache.hc.core5.util.Timeout;
3543

3644
import org.springframework.beans.factory.FactoryBean;
@@ -41,6 +49,7 @@
4149
* HttpClient 5.
4250
*
4351
* @author Lars Uffmann
52+
* @author Stephane Nicoll
4453
* @since 4.0.5
4554
* @see <a href=https://hc.apache.org/httpcomponents-client>HttpComponents</a>
4655
*/
@@ -60,6 +69,10 @@ public class HttpComponents5ClientFactory implements FactoryBean<CloseableHttpCl
6069

6170
private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(60);
6271

72+
private final List<HttpClientBuilderCustomizer> clientBuilderCustomizers = new ArrayList<>();
73+
74+
private final List<PoolingHttpClientConnectionManagerBuilderCustomizer> connectionManagerBuilderCustomizers = new ArrayList<>();
75+
6376
private Duration connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
6477

6578
private Duration readTimeout = DEFAULT_READ_TIMEOUT;
@@ -74,9 +87,62 @@ public class HttpComponents5ClientFactory implements FactoryBean<CloseableHttpCl
7487

7588
private PoolingHttpClientConnectionManager connectionManager;
7689

77-
private HttpClientBuilderCustomizer clientBuilderCustomizer;
90+
/**
91+
* Create a new instance with default settings. This configures
92+
* {@link RemoveSoapHeadersInterceptor} as the first interceptor
93+
* @return a factory with default settings
94+
*/
95+
public static HttpComponents5ClientFactory withDefaults() {
96+
HttpComponents5ClientFactory factory = new HttpComponents5ClientFactory();
97+
factory.addClientBuilderCustomizer((httpClientBuilder) -> httpClientBuilder
98+
.addRequestInterceptorFirst(new RemoveSoapHeadersInterceptor()));
99+
return factory;
100+
}
101+
102+
/**
103+
* Add a {@link HttpClientBuilderCustomizer} to invoke when creating an
104+
* {@link CloseableHttpClient} managed by this factory.
105+
* @param clientBuilderCustomizer the customizer to invoke
106+
* @since 4.1.0
107+
*/
108+
public void addClientBuilderCustomizer(HttpClientBuilderCustomizer clientBuilderCustomizer) {
109+
this.clientBuilderCustomizers.add(clientBuilderCustomizer);
110+
}
111+
112+
/**
113+
* Add a {@link HttpClientBuilderCustomizer} to invoke when creating an
114+
* {@link CloseableHttpClient} managed by this factory.
115+
* @param clientBuilderCustomizer the customizer to invoke
116+
* @deprecated as of 4.1.0 in favor of
117+
* {@link #addClientBuilderCustomizer(HttpClientBuilderCustomizer)}l
118+
*/
119+
@Deprecated(since = "4.1.0", forRemoval = true)
120+
public void setClientBuilderCustomizer(HttpClientBuilderCustomizer clientBuilderCustomizer) {
121+
addClientBuilderCustomizer(clientBuilderCustomizer);
122+
}
123+
124+
/**
125+
* Add a {@link PoolingHttpClientConnectionManagerBuilderCustomizer} to invoke when
126+
* creating an {@link CloseableHttpClient} managed by this factory.
127+
* @param connectionManagerBuilderCustomizer the customizer to invoke
128+
*/
129+
public void addConnectionManagerBuilderCustomizer(
130+
PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer) {
131+
this.connectionManagerBuilderCustomizers.add(connectionManagerBuilderCustomizer);
132+
}
78133

79-
private PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer;
134+
/**
135+
* Add a {@link PoolingHttpClientConnectionManagerBuilderCustomizer} to invoke when
136+
* creating an {@link CloseableHttpClient} managed by this factory.
137+
* @param connectionManagerBuilderCustomizer the customizer to invoke
138+
* @deprecated as of 4.1.0 in favor of
139+
* {@link #addConnectionManagerBuilderCustomizer(PoolingHttpClientConnectionManagerBuilderCustomizer)}
140+
*/
141+
@Deprecated(since = "4.1.0", forRemoval = true)
142+
public void setConnectionManagerBuilderCustomizer(
143+
PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer) {
144+
addConnectionManagerBuilderCustomizer(connectionManagerBuilderCustomizer);
145+
}
80146

81147
/**
82148
* Sets the credentials to be used. If not set, no authentication is done.
@@ -104,11 +170,9 @@ public void setAuthScope(AuthScope authScope) {
104170
* @param timeout the timeout value
105171
*/
106172
public void setConnectionTimeout(Duration timeout) {
107-
108173
if (timeout.isNegative()) {
109174
throw new IllegalArgumentException("timeout must be a non-negative value");
110175
}
111-
112176
this.connectionTimeout = timeout;
113177
}
114178

@@ -118,11 +182,9 @@ public void setConnectionTimeout(Duration timeout) {
118182
* @param timeout the timeout value
119183
*/
120184
public void setReadTimeout(Duration timeout) {
121-
122185
if (timeout.isNegative()) {
123186
throw new IllegalArgumentException("timeout must be a non-negative value");
124187
}
125-
126188
this.readTimeout = timeout;
127189
}
128190

@@ -132,11 +194,9 @@ public void setReadTimeout(Duration timeout) {
132194
* @see PoolingHttpClientConnectionManager
133195
*/
134196
public void setMaxTotalConnections(int maxTotalConnections) {
135-
136197
if (maxTotalConnections <= 0) {
137198
throw new IllegalArgumentException("maxTotalConnections must be a positive value");
138199
}
139-
140200
this.maxTotalConnections = maxTotalConnections;
141201
}
142202

@@ -160,11 +220,44 @@ public void setMaxConnectionsPerHost(Map<String, String> maxConnectionsPerHost)
160220
this.maxConnectionsPerHost = maxConnectionsPerHost;
161221
}
162222

163-
void applyMaxConnectionsPerHost(PoolingHttpClientConnectionManager connectionManager) throws URISyntaxException {
223+
PoolingHttpClientConnectionManager getConnectionManager() {
224+
return this.connectionManager;
225+
}
164226

165-
for (Map.Entry<String, String> entry : this.maxConnectionsPerHost.entrySet()) {
227+
@SuppressWarnings("deprecation")
228+
public CloseableHttpClient build() {
229+
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
230+
.create();
231+
if (this.maxTotalConnections != -1) {
232+
connectionManagerBuilder.setMaxConnTotal(this.maxTotalConnections);
233+
}
234+
this.connectionManagerBuilderCustomizers.forEach(customizer -> customizer.customize(connectionManagerBuilder));
235+
this.connectionManager = connectionManagerBuilder.build();
236+
237+
applyMaxConnectionsPerHost(this.connectionManager);
238+
239+
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom()
240+
.setConnectTimeout(Timeout.of(this.connectionTimeout))
241+
.setResponseTimeout(Timeout.of(this.readTimeout));
242+
243+
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
244+
.setDefaultRequestConfig(requestConfigBuilder.build())
245+
.setConnectionManager(this.connectionManager);
246+
247+
if (this.credentials != null && this.authScope != null) {
248+
BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
249+
basicCredentialsProvider.setCredentials(this.authScope, this.credentials);
250+
httpClientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider);
251+
}
252+
253+
this.clientBuilderCustomizers.forEach(customizer -> customizer.customize(httpClientBuilder));
166254

167-
URI uri = new URI(entry.getKey());
255+
return httpClientBuilder.build();
256+
}
257+
258+
void applyMaxConnectionsPerHost(PoolingHttpClientConnectionManager connectionManager) {
259+
for (Map.Entry<String, String> entry : this.maxConnectionsPerHost.entrySet()) {
260+
URI uri = URI.create(entry.getKey());
168261
HttpHost host = new HttpHost(uri.getScheme(), uri.getHost(), getPort(uri));
169262
final HttpRoute route;
170263

@@ -180,17 +273,14 @@ void applyMaxConnectionsPerHost(PoolingHttpClientConnectionManager connectionMan
180273
}
181274

182275
static int getPort(URI uri) {
183-
184276
if (uri.getPort() == -1) {
185-
186277
if ("https".equalsIgnoreCase(uri.getScheme())) {
187278
return 443;
188279
}
189280
if ("http".equalsIgnoreCase(uri.getScheme())) {
190281
return 80;
191282
}
192283
}
193-
194284
return uri.getPort();
195285
}
196286

@@ -200,62 +290,36 @@ public boolean isSingleton() {
200290
}
201291

202292
@Override
203-
@SuppressWarnings("deprecation")
204293
public CloseableHttpClient getObject() throws Exception {
205-
206-
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
207-
.create();
208-
209-
if (this.maxTotalConnections != -1) {
210-
connectionManagerBuilder.setMaxConnTotal(this.maxTotalConnections);
211-
}
212-
213-
if (this.connectionManagerBuilderCustomizer != null) {
214-
this.connectionManagerBuilderCustomizer.customize(connectionManagerBuilder);
215-
}
216-
217-
this.connectionManager = connectionManagerBuilder.build();
218-
219-
applyMaxConnectionsPerHost(this.connectionManager);
220-
221-
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom()
222-
.setConnectTimeout(Timeout.of(this.connectionTimeout))
223-
.setResponseTimeout(Timeout.of(this.readTimeout));
224-
225-
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
226-
.setDefaultRequestConfig(requestConfigBuilder.build())
227-
.setConnectionManager(this.connectionManager);
228-
229-
if (this.credentials != null && this.authScope != null) {
230-
231-
BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
232-
basicCredentialsProvider.setCredentials(this.authScope, this.credentials);
233-
httpClientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider);
234-
}
235-
236-
if (this.clientBuilderCustomizer != null) {
237-
this.clientBuilderCustomizer.customize(httpClientBuilder);
238-
}
239-
240-
return httpClientBuilder.build();
294+
return build();
241295
}
242296

243297
@Override
244298
public Class<?> getObjectType() {
245299
return CloseableHttpClient.class;
246300
}
247301

248-
PoolingHttpClientConnectionManager getConnectionManager() {
249-
return this.connectionManager;
250-
}
302+
/**
303+
* HttpClient {@link HttpRequestInterceptor} implementation that removes
304+
* {@code Content-Length} and {@code Transfer-Encoding} headers from the request.
305+
* Necessary, because some SAAJ and other SOAP implementations set these headers
306+
* themselves, and HttpClient throws an exception if they have been set.
307+
*/
308+
public static class RemoveSoapHeadersInterceptor implements HttpRequestInterceptor {
251309

252-
public void setClientBuilderCustomizer(HttpClientBuilderCustomizer clientBuilderCustomizer) {
253-
this.clientBuilderCustomizer = clientBuilderCustomizer;
254-
}
310+
@Override
311+
public void process(HttpRequest request, EntityDetails entityDetails, HttpContext httpContext)
312+
throws HttpException, IOException {
313+
314+
if (request.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
315+
request.removeHeaders(HttpHeaders.TRANSFER_ENCODING);
316+
}
317+
318+
if (request.containsHeader(HttpHeaders.CONTENT_LENGTH)) {
319+
request.removeHeaders(HttpHeaders.CONTENT_LENGTH);
320+
}
321+
}
255322

256-
public void setConnectionManagerBuilderCustomizer(
257-
PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer) {
258-
this.connectionManagerBuilderCustomizer = connectionManagerBuilderCustomizer;
259323
}
260324

261325
@FunctionalInterface

spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5MessageSender.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.beans.factory.InitializingBean;
3030
import org.springframework.util.Assert;
31+
import org.springframework.ws.transport.http.HttpComponentsMessageSender.RemoveSoapHeadersInterceptor;
3132

3233
/**
3334
* {@code AbstractHttpComponents5MessageSender} implementation that configures the
@@ -58,9 +59,7 @@ public class HttpComponents5MessageSender extends AbstractHttpComponents5Message
5859
* {@link HttpClient} that uses a default {@link PoolingHttpClientConnectionManager}.
5960
*/
6061
public HttpComponents5MessageSender() {
61-
this.clientFactory = new HttpComponents5ClientFactory();
62-
this.clientFactory.setClientBuilderCustomizer(
63-
httpClientBuilder -> httpClientBuilder.addRequestInterceptorFirst(new RemoveSoapHeadersInterceptor()));
62+
this.clientFactory = HttpComponents5ClientFactory.withDefaults();
6463
}
6564

6665
/**

spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ public SimpleHttpComponents5MessageSender(HttpClient httpClient) {
5353
* Create a new instance with the state of the given
5454
* {@link HttpComponents5ClientFactory}.
5555
* @param factory the factory to use
56-
* @throws Exception if the client fails to build
5756
*/
58-
public SimpleHttpComponents5MessageSender(HttpComponents5ClientFactory factory) throws Exception {
59-
this(factory.getObject());
57+
public SimpleHttpComponents5MessageSender(HttpComponents5ClientFactory factory) {
58+
this(factory.build());
6059
}
6160

6261
@Override

0 commit comments

Comments
 (0)