diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java index 1256a5bc..51e5dbcc 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java @@ -22,12 +22,7 @@ import java.net.URI; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.protocol.HttpContext; import org.springframework.beans.factory.DisposableBean; @@ -84,27 +79,4 @@ public abstract class AbstractHttpComponents5MessageSender extends AbstractHttpW return null; } - /** - * HttpClient {@link HttpRequestInterceptor} implementation that removes - * {@code Content-Length} and {@code Transfer-Encoding} headers from the request. - * Necessary, because some SAAJ and other SOAP implementations set these headers - * themselves, and HttpClient throws an exception if they have been set. - */ - public static class RemoveSoapHeadersInterceptor implements HttpRequestInterceptor { - - @Override - public void process(HttpRequest request, EntityDetails entityDetails, HttpContext httpContext) - throws HttpException, IOException { - - if (request.containsHeader(HttpHeaders.TRANSFER_ENCODING)) { - request.removeHeaders(HttpHeaders.TRANSFER_ENCODING); - } - - if (request.containsHeader(HttpHeaders.CONTENT_LENGTH)) { - request.removeHeaders(HttpHeaders.CONTENT_LENGTH); - } - } - - } - } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java index dd395ef6..c2843259 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java @@ -16,9 +16,11 @@ package org.springframework.ws.transport.http; +import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.apache.hc.client5.http.HttpRoute; @@ -30,7 +32,13 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.util.Timeout; import org.springframework.beans.factory.FactoryBean; @@ -41,6 +49,7 @@ import org.springframework.ws.client.support.interceptor.ClientInterceptor; * HttpClient 5. * * @author Lars Uffmann + * @author Stephane Nicoll * @since 4.0.5 * @see HttpComponents */ @@ -60,6 +69,10 @@ public class HttpComponents5ClientFactory implements FactoryBean clientBuilderCustomizers = new ArrayList<>(); + + private final List connectionManagerBuilderCustomizers = new ArrayList<>(); + private Duration connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; private Duration readTimeout = DEFAULT_READ_TIMEOUT; @@ -74,9 +87,62 @@ public class HttpComponents5ClientFactory implements FactoryBean httpClientBuilder + .addRequestInterceptorFirst(new RemoveSoapHeadersInterceptor())); + return factory; + } - private PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer; + /** + * Add a {@link HttpClientBuilderCustomizer} to invoke when creating an + * {@link CloseableHttpClient} managed by this factory. + * @param clientBuilderCustomizer the customizer to invoke + * @since 4.1.0 + */ + public void addClientBuilderCustomizer(HttpClientBuilderCustomizer clientBuilderCustomizer) { + this.clientBuilderCustomizers.add(clientBuilderCustomizer); + } + + /** + * Add a {@link HttpClientBuilderCustomizer} to invoke when creating an + * {@link CloseableHttpClient} managed by this factory. + * @param clientBuilderCustomizer the customizer to invoke + * @deprecated as of 4.1.0 in favor of + * {@link #addClientBuilderCustomizer(HttpClientBuilderCustomizer)}l + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public void setClientBuilderCustomizer(HttpClientBuilderCustomizer clientBuilderCustomizer) { + addClientBuilderCustomizer(clientBuilderCustomizer); + } + + /** + * Add a {@link PoolingHttpClientConnectionManagerBuilderCustomizer} to invoke when + * creating an {@link CloseableHttpClient} managed by this factory. + * @param connectionManagerBuilderCustomizer the customizer to invoke + */ + public void addConnectionManagerBuilderCustomizer( + PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer) { + this.connectionManagerBuilderCustomizers.add(connectionManagerBuilderCustomizer); + } + + /** + * Add a {@link PoolingHttpClientConnectionManagerBuilderCustomizer} to invoke when + * creating an {@link CloseableHttpClient} managed by this factory. + * @param connectionManagerBuilderCustomizer the customizer to invoke + * @deprecated as of 4.1.0 in favor of + * {@link #addConnectionManagerBuilderCustomizer(PoolingHttpClientConnectionManagerBuilderCustomizer)} + */ + @Deprecated(since = "4.1.0", forRemoval = true) + public void setConnectionManagerBuilderCustomizer( + PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer) { + addConnectionManagerBuilderCustomizer(connectionManagerBuilderCustomizer); + } /** * Sets the credentials to be used. If not set, no authentication is done. @@ -104,11 +170,9 @@ public class HttpComponents5ClientFactory implements FactoryBean customizer.customize(connectionManagerBuilder)); + this.connectionManager = connectionManagerBuilder.build(); + + applyMaxConnectionsPerHost(this.connectionManager); + + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom() + .setConnectTimeout(Timeout.of(this.connectionTimeout)) + .setResponseTimeout(Timeout.of(this.readTimeout)); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfigBuilder.build()) + .setConnectionManager(this.connectionManager); + + if (this.credentials != null && this.authScope != null) { + BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider(); + basicCredentialsProvider.setCredentials(this.authScope, this.credentials); + httpClientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider); + } + + this.clientBuilderCustomizers.forEach(customizer -> customizer.customize(httpClientBuilder)); + + return httpClientBuilder.build(); + } + + void applyMaxConnectionsPerHost(PoolingHttpClientConnectionManager connectionManager) { for (Map.Entry entry : this.maxConnectionsPerHost.entrySet()) { - - URI uri = new URI(entry.getKey()); + URI uri = URI.create(entry.getKey()); HttpHost host = new HttpHost(uri.getScheme(), uri.getHost(), getPort(uri)); final HttpRoute route; @@ -180,9 +273,7 @@ public class HttpComponents5ClientFactory implements FactoryBean httpClientBuilder.addRequestInterceptorFirst(new RemoveSoapHeadersInterceptor())); + this.clientFactory = HttpComponents5ClientFactory.withDefaults(); } /** diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java index c1fa0225..2267778e 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java @@ -53,10 +53,9 @@ public class SimpleHttpComponents5MessageSender extends AbstractHttpComponents5M * Create a new instance with the state of the given * {@link HttpComponents5ClientFactory}. * @param factory the factory to use - * @throws Exception if the client fails to build */ - public SimpleHttpComponents5MessageSender(HttpComponents5ClientFactory factory) throws Exception { - this(factory.getObject()); + public SimpleHttpComponents5MessageSender(HttpComponents5ClientFactory factory) { + this(factory.build()); } @Override diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ClientFactoryTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ClientFactoryTest.java new file mode 100644 index 00000000..b929374b --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ClientFactoryTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2005-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ws.transport.http; + +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.ws.transport.http.HttpComponents5ClientFactory.HttpClientBuilderCustomizer; +import org.springframework.ws.transport.http.HttpComponents5ClientFactory.PoolingHttpClientConnectionManagerBuilderCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpComponents5ClientFactory}. + * + * @author Stephane Nicoll + */ +class HttpComponents5ClientFactoryTest { + + @Test + void httpclientBuilderCustomizersAreCalledInOrder() { + HttpClientBuilderCustomizer first = mock(HttpClientBuilderCustomizer.class); + HttpClientBuilderCustomizer second = mock(HttpClientBuilderCustomizer.class); + HttpComponents5ClientFactory factory = new HttpComponents5ClientFactory(); + factory.addClientBuilderCustomizer(first); + factory.addClientBuilderCustomizer(second); + assertThat(factory.build()).isNotNull(); + InOrder inOrder = inOrder(first, second); + inOrder.verify(first).customize(any()); + inOrder.verify(second).customize(any()); + } + + @Test + void connectionManagerBuilderCustomizerCustomizersAreCalledInOrder() { + PoolingHttpClientConnectionManagerBuilderCustomizer first = mock( + PoolingHttpClientConnectionManagerBuilderCustomizer.class); + PoolingHttpClientConnectionManagerBuilderCustomizer second = mock( + PoolingHttpClientConnectionManagerBuilderCustomizer.class); + HttpComponents5ClientFactory factory = new HttpComponents5ClientFactory(); + factory.addConnectionManagerBuilderCustomizer(first); + factory.addConnectionManagerBuilderCustomizer(second); + assertThat(factory.build()).isNotNull(); + InOrder inOrder = inOrder(first, second); + inOrder.verify(first).customize(any()); + inOrder.verify(second).customize(any()); + } + +} \ No newline at end of file diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java index 930c4952..af759f0c 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java @@ -17,8 +17,6 @@ package org.springframework.ws.transport.http; import org.apache.hc.client5.http.classic.ExecChainHandler; -import org.apache.hc.client5.http.classic.HttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -31,11 +29,10 @@ class HttpComponents5ContentTypeIntegrationTest assertThat(request.getEntity().getContentType()).isNotBlank(); return chain.proceed(request, scope); }; - HttpClient client = HttpClientBuilder.create() - .addRequestInterceptorFirst(new HttpComponents5MessageSender.RemoveSoapHeadersInterceptor()) - .addExecInterceptorFirst("handler with assertion", testHandler) - .build(); - return new SimpleHttpComponents5MessageSender(client); + HttpComponents5ClientFactory factory = HttpComponents5ClientFactory.withDefaults(); + factory.addClientBuilderCustomizer( + builder -> builder.addExecInterceptorFirst("handler with assertion", testHandler)); + return new SimpleHttpComponents5MessageSender(factory.build()); } } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java index 8149fec2..2ddbbd06 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java @@ -51,10 +51,10 @@ class SimpleHttpComponents5MessageSenderTest { } @Test - void createWithFactory() throws Exception { + void createWithFactory() { HttpComponents5ClientFactory factory = new HttpComponents5ClientFactory(); HttpClientBuilderCustomizer builderCustomizer = mock(HttpClientBuilderCustomizer.class); - factory.setClientBuilderCustomizer(builderCustomizer); + factory.addClientBuilderCustomizer(builderCustomizer); new SimpleHttpComponents5MessageSender(factory); verify(builderCustomizer).customize(any()); }