Introduce `ClientHttpRequestFactoryBuilder` support

Add a new `ClientHttpRequestFactoryBuilder` interface to support the
creation of `ClientHttpRequestFactory` instances. The new code will
ultimately replace the existing `ClientHttpRequestFactories` class.

The `ClientHttpRequestFactoryBuilder` is a functional interface with
additional static factory methods for the various supported
`ClientHttpRequestFactory` types. Each type has it's own builder
which should allow us to support additional customization in the future.

Unlike `ClientHttpRequestFactories`, the builder aligns with Spring
Framework defaults and will detect the `JdkClientHttpRequestFactory`
in preference of `SimpleClientHttpRequestFactory`.

This commit also relocates `ClientHttpRequestFactorySettings` to bring
it into the new `http.client` package.

See gh-36266
This commit is contained in:
Phillip Webb 2024-10-24 00:09:13 -07:00
parent 0a4ac283d0
commit 78458afa17
23 changed files with 2006 additions and 24 deletions

View File

@ -0,0 +1,72 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.Assert;
/**
* Internal base class used for {@link ClientHttpRequestFactoryBuilder} implementations.
*
* @param <T> the {@link ClientHttpRequestFactory} type
* @author Phillip Webb
*/
abstract class AbstractClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory>
implements ClientHttpRequestFactoryBuilder<T> {
private final List<Consumer<T>> customizers;
protected AbstractClientHttpRequestFactoryBuilder(List<Consumer<T>> customizers) {
this.customizers = (customizers != null) ? customizers : Collections.emptyList();
}
protected final List<Consumer<T>> mergedCustomizers(Consumer<T> customizer) {
Assert.notNull(this.customizers, "'customizer' must not be null");
return merge(this.customizers, List.of(customizer));
}
protected final List<Consumer<T>> mergedCustomizers(Collection<Consumer<T>> customizers) {
Assert.notNull(customizers, "'customizers' must not be null");
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
return merge(this.customizers, customizers);
}
private <E> List<E> merge(Collection<E> list, Collection<? extends E> additional) {
List<E> merged = new ArrayList<>(list);
merged.addAll(additional);
return List.copyOf(merged);
}
@Override
@SuppressWarnings("unchecked")
public final T build(ClientHttpRequestFactorySettings settings) {
T factory = createClientHttpRequestFactory(
(settings != null) ? settings : ClientHttpRequestFactorySettings.defaults());
LambdaSafe.callbacks(Consumer.class, this.customizers, factory).invoke((consumer) -> consumer.accept(factory));
return factory;
}
protected abstract T createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings);
}

View File

@ -0,0 +1,214 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
/**
* Interface used to build a fully configured {@link ClientHttpRequestFactory}. Builders
* for {@link #httpComponents() Apache HTTP Components}, {@link #jetty() Jetty},
* {@link #reactor() Reactor}, {@link #jdk() JDK} and {@link #simple() simple client} can
* be obtained using the factory methods on this interface. The {@link #of(Class)} and
* {@link #of(Supplier)} methods may be used to instantiate other
* {@link ClientHttpRequestFactory} instances using reflection.
*
* @param <T> the {@link ClientHttpRequestFactory} type
* @author Phillip Webb
* @since 3.4.0
*/
@FunctionalInterface
public interface ClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory> {
/**
* Build a default configured {@link ClientHttpRequestFactory}.
* @return a default configured {@link ClientHttpRequestFactory}.
*/
default T build() {
return build(null);
}
/**
* Build a fully configured {@link ClientHttpRequestFactory}, applying the given
* {@code settings} if they are provided.
* @param settings the settings to apply or {@code null}
* @return a fully configured {@link ClientHttpRequestFactory}.
*/
T build(ClientHttpRequestFactorySettings settings);
/**
* Return a new {@link ClientHttpRequestFactoryBuilder} that applies the given
* customizer to the {@link ClientHttpRequestFactory} after it has been built.
* @param customizer the customizers to apply
* @return a new {@link ClientHttpRequestFactoryBuilder} instance
*/
default ClientHttpRequestFactoryBuilder<T> withCustomizer(Consumer<T> customizer) {
return withCustomizers(List.of(customizer));
}
/**
* Return a new {@link ClientHttpRequestFactoryBuilder} that applies the given
* customizers to the {@link ClientHttpRequestFactory} after it has been built.
* @param customizers the customizers to apply
* @return a new {@link ClientHttpRequestFactoryBuilder} instance
*/
@SuppressWarnings("unchecked")
default ClientHttpRequestFactoryBuilder<T> withCustomizers(Collection<Consumer<T>> customizers) {
Assert.notNull(customizers, "'customizers' must not be null");
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
return (settings) -> {
T factory = build(settings);
LambdaSafe.callbacks(Consumer.class, customizers, factory).invoke((consumer) -> consumer.accept(factory));
return factory;
};
}
/**
* Return a {@link HttpComponentsClientHttpRequestFactoryBuilder} that can be used to
* build a {@link HttpComponentsClientHttpRequestFactory}.
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder}
*/
static HttpComponentsClientHttpRequestFactoryBuilder httpComponents() {
return new HttpComponentsClientHttpRequestFactoryBuilder();
}
/**
* Return a {@link JettyClientHttpRequestFactoryBuilder} that can be used to build a
* {@link JettyClientHttpRequestFactory}.
* @return a new {@link JettyClientHttpRequestFactoryBuilder}
*/
static JettyClientHttpRequestFactoryBuilder jetty() {
return new JettyClientHttpRequestFactoryBuilder();
}
/**
* Return a {@link ReactorClientHttpRequestFactoryBuilder} that can be used to build a
* {@link ReactorClientHttpRequestFactory}.
* @return a new {@link ReactorClientHttpRequestFactoryBuilder}
*/
static ReactorClientHttpRequestFactoryBuilder reactor() {
return new ReactorClientHttpRequestFactoryBuilder();
}
/**
* Return a {@link JdkClientHttpRequestFactoryBuilder} that can be used to build a
* {@link JdkClientHttpRequestFactory} .
* @return a new {@link JdkClientHttpRequestFactoryBuilder}
*/
static JdkClientHttpRequestFactoryBuilder jdk() {
return new JdkClientHttpRequestFactoryBuilder();
}
/**
* Return a {@link SimpleClientHttpRequestFactoryBuilder} that can be used to build a
* {@link SimpleClientHttpRequestFactory} .
* @return a new {@link SimpleClientHttpRequestFactoryBuilder}
*/
static SimpleClientHttpRequestFactoryBuilder simple() {
return new SimpleClientHttpRequestFactoryBuilder();
}
/**
* Return a new {@link ClientHttpRequestFactoryBuilder} for the given
* {@code requestFactoryType}. The following implementations are supported without the
* use of reflection:
* <ul>
* <li>{@link HttpComponentsClientHttpRequestFactory}</li>
* <li>{@link JdkClientHttpRequestFactory}</li>
* <li>{@link JettyClientHttpRequestFactory}</li>
* <li>{@link ReactorClientHttpRequestFactory}</li>
* <li>{@link SimpleClientHttpRequestFactory}</li>
* </ul>
* @param <T> the {@link ClientHttpRequestFactory} type
* @param requestFactoryType the {@link ClientHttpRequestFactory} type
* @return a new {@link ClientHttpRequestFactoryBuilder}
*/
@SuppressWarnings("unchecked")
static <T extends ClientHttpRequestFactory> ClientHttpRequestFactoryBuilder<T> of(Class<T> requestFactoryType) {
Assert.notNull(requestFactoryType, "'requestFactoryType' must not be null");
Assert.isTrue(requestFactoryType != ClientHttpRequestFactory.class,
"'requestFactoryType' must be an implementation of ClientHttpRequestFactory");
if (requestFactoryType == HttpComponentsClientHttpRequestFactory.class) {
return (ClientHttpRequestFactoryBuilder<T>) httpComponents();
}
if (requestFactoryType == JettyClientHttpRequestFactory.class) {
return (ClientHttpRequestFactoryBuilder<T>) jetty();
}
if (requestFactoryType == ReactorClientHttpRequestFactory.class) {
return (ClientHttpRequestFactoryBuilder<T>) reactor();
}
if (requestFactoryType == JdkClientHttpRequestFactory.class) {
return (ClientHttpRequestFactoryBuilder<T>) jdk();
}
if (requestFactoryType == SimpleClientHttpRequestFactory.class) {
return (ClientHttpRequestFactoryBuilder<T>) simple();
}
return new ReflectiveComponentsClientHttpRequestFactoryBuilder<>(requestFactoryType);
}
/**
* Return a new {@link ClientHttpRequestFactoryBuilder} from the given supplier, using
* reflection to ultimately apply the {@link ClientHttpRequestFactorySettings}.
* @param <T> the {@link ClientHttpRequestFactory} type
* @param requestFactorySupplier the {@link ClientHttpRequestFactory} supplier
* @return a new {@link ClientHttpRequestFactoryBuilder}
*/
static <T extends ClientHttpRequestFactory> ClientHttpRequestFactoryBuilder<T> of(
Supplier<T> requestFactorySupplier) {
return new ReflectiveComponentsClientHttpRequestFactoryBuilder<>(requestFactorySupplier);
}
/**
* Detect the most suitable {@link ClientHttpRequestFactoryBuilder} based on the
* classpath. The methods favors builders in the following order:
* <ol>
* <li>{@link #httpComponents()}</li>
* <li>{@link #jetty()}</li>
* <li>{@link #reactor()}</li>
* <li>{@link #jdk()}</li>
* <li>{@link #simple()}</li>
* </ol>
* @return the most suitable {@link ClientHttpRequestFactoryBuilder} for the classpath
*/
static ClientHttpRequestFactoryBuilder<? extends ClientHttpRequestFactory> detect() {
if (HttpComponentsClientHttpRequestFactoryBuilder.Classes.PRESENT) {
return httpComponents();
}
if (JettyClientHttpRequestFactoryBuilder.Classes.PRESENT) {
return jetty();
}
if (ReactorClientHttpRequestFactoryBuilder.Classes.PRESENT) {
return reactor();
}
if (JdkClientHttpRequestFactoryBuilder.Classes.PRESENT) {
return jdk();
}
return simple();
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.web.client;
package org.springframework.boot.http.client;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@ -28,6 +28,7 @@ import org.springframework.aot.hint.TypeReference;
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@ -36,12 +37,12 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@link RuntimeHintsRegistrar} for {@link ClientHttpRequestFactories}.
* {@link RuntimeHintsRegistrar} for {@link ClientHttpRequestFactory} implementations.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ClientHttpRequestFactoriesRuntimeHints implements RuntimeHintsRegistrar {
class ClientHttpRequestFactoryRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
@ -52,21 +53,29 @@ class ClientHttpRequestFactoriesRuntimeHints implements RuntimeHintsRegistrar {
private void registerHints(ReflectionHints hints, ClassLoader classLoader) {
hints.registerField(findField(AbstractClientHttpRequestFactoryWrapper.class, "requestFactory"));
hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.APACHE_HTTP_CLIENT_CLASS, (typeHint) -> {
typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.APACHE_HTTP_CLIENT_CLASS));
registerReflectionHints(hints, HttpComponentsClientHttpRequestFactory.class);
});
hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.JETTY_CLIENT_CLASS, (typeHint) -> {
typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.JETTY_CLIENT_CLASS));
registerReflectionHints(hints, JettyClientHttpRequestFactory.class, long.class);
});
registerClientHttpRequestFactoryHints(hints, classLoader,
HttpComponentsClientHttpRequestFactoryBuilder.Classes.HTTP_CLIENTS,
() -> registerReflectionHints(hints, HttpComponentsClientHttpRequestFactory.class));
registerClientHttpRequestFactoryHints(hints, classLoader,
JettyClientHttpRequestFactoryBuilder.Classes.HTTP_CLIENT,
() -> registerReflectionHints(hints, JettyClientHttpRequestFactory.class, long.class));
registerClientHttpRequestFactoryHints(hints, classLoader,
ReactorClientHttpRequestFactoryBuilder.Classes.HTTP_CLIENT,
() -> registerReflectionHints(hints, ReactorClientHttpRequestFactory.class, long.class));
registerClientHttpRequestFactoryHints(hints, classLoader,
JdkClientHttpRequestFactoryBuilder.Classes.HTTP_CLIENT,
() -> registerReflectionHints(hints, JdkClientHttpRequestFactory.class));
hints.registerType(SimpleClientHttpRequestFactory.class, (typeHint) -> {
typeHint.onReachableType(HttpURLConnection.class);
registerReflectionHints(hints, SimpleClientHttpRequestFactory.class);
});
hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.REACTOR_CLIENT_CLASS, (typeHint) -> {
typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.REACTOR_CLIENT_CLASS));
registerReflectionHints(hints, ReactorClientHttpRequestFactory.class, long.class);
}
private void registerClientHttpRequestFactoryHints(ReflectionHints hints, ClassLoader classLoader, String className,
Runnable action) {
hints.registerTypeIfPresent(classLoader, className, (typeHint) -> {
typeHint.onReachableType(TypeReference.of(className));
action.run();
});
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.time.Duration;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.ClientHttpRequestFactory;
/**
* Settings that can be applied when creating a {@link ClientHttpRequestFactory}.
*
* @param connectTimeout the connect timeout
* @param readTimeout the read timeout
* @param sslBundle the SSL bundle providing SSL configuration
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @since 3.4.0
* @see ClientHttpRequestFactoryBuilder
*/
public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, SslBundle sslBundle) {
private static final ClientHttpRequestFactorySettings defaults = new ClientHttpRequestFactorySettings(null, null,
null);
/**
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
* connect timeout setting .
* @param connectTimeout the new connect timeout setting
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeout) {
return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.sslBundle);
}
/**
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated read
* timeout setting.
* @param readTimeout the new read timeout setting
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.sslBundle);
}
/**
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated SSL
* bundle setting.
* @param sslBundle the new SSL bundle setting
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public ClientHttpRequestFactorySettings withSslBundle(SslBundle sslBundle) {
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, sslBundle);
}
/**
* Return a new {@link ClientHttpRequestFactorySettings} using defaults for all
* settings other than the provided SSL bundle.
* @param sslBundle the SSL bundle setting
* @return a new {@link ClientHttpRequestFactorySettings} instance
*/
public static ClientHttpRequestFactorySettings ofSslBundle(SslBundle sslBundle) {
return defaults().withSslBundle(sslBundle);
}
/**
* Use defaults for the {@link ClientHttpRequestFactory} which can differ depending on
* the implementation.
* @return default settings
*/
public static ClientHttpRequestFactorySettings defaults() {
return defaults;
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.apache.hc.client5.http.classic.HttpClient;
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.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.core5.http.io.SocketConfig;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.ClassUtils;
/**
* Builder for {@link ClientHttpRequestFactoryBuilder#httpComponents()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
* @since 3.4.0
*/
public final class HttpComponentsClientHttpRequestFactoryBuilder
extends AbstractClientHttpRequestFactoryBuilder<HttpComponentsClientHttpRequestFactory> {
HttpComponentsClientHttpRequestFactoryBuilder() {
this(Collections.emptyList());
}
private HttpComponentsClientHttpRequestFactoryBuilder(
List<Consumer<HttpComponentsClientHttpRequestFactory>> customizers) {
super(customizers);
}
@Override
public HttpComponentsClientHttpRequestFactoryBuilder withCustomizer(
Consumer<HttpComponentsClientHttpRequestFactory> customizer) {
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer));
}
@Override
public HttpComponentsClientHttpRequestFactoryBuilder withCustomizers(
Collection<Consumer<HttpComponentsClientHttpRequestFactory>> customizers) {
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizers));
}
@Override
protected HttpComponentsClientHttpRequestFactory createClientHttpRequestFactory(
ClientHttpRequestFactorySettings settings) {
HttpClient httpClient = createHttpClient(settings.readTimeout(), settings.sslBundle());
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(factory::setConnectTimeout);
return factory;
}
private HttpClient createHttpClient(Duration readTimeout, SslBundle sslBundle) {
return HttpClientBuilder.create()
.useSystemProperties()
.setConnectionManager(createConnectionManager(readTimeout, sslBundle))
.build();
}
private PoolingHttpClientConnectionManager createConnectionManager(Duration readTimeout, SslBundle sslBundle) {
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
.create();
if (readTimeout != null) {
connectionManagerBuilder.setDefaultSocketConfig(createSocketConfig(readTimeout));
}
if (sslBundle != null) {
connectionManagerBuilder.setTlsSocketStrategy(createTlsSocketStrategy(sslBundle));
}
PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.useSystemProperties().build();
return connectionManager;
}
private DefaultClientTlsStrategy createTlsSocketStrategy(SslBundle sslBundle) {
SslOptions options = sslBundle.getOptions();
DefaultClientTlsStrategy tlsSocketStrategy = new DefaultClientTlsStrategy(sslBundle.createSslContext(),
options.getEnabledProtocols(), options.getCiphers(), null, new DefaultHostnameVerifier());
return tlsSocketStrategy;
}
private SocketConfig createSocketConfig(Duration readTimeout) {
return SocketConfig.custom().setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS).build();
}
static class Classes {
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.classic.HttpClients";
static final boolean PRESENT = ClassUtils.isPresent(HTTP_CLIENTS, null);
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.util.ClassUtils;
/**
* Builder for {@link ClientHttpRequestFactoryBuilder#jdk()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
* @since 3.4.0
*/
public class JdkClientHttpRequestFactoryBuilder
extends AbstractClientHttpRequestFactoryBuilder<JdkClientHttpRequestFactory> {
JdkClientHttpRequestFactoryBuilder() {
this(null);
}
private JdkClientHttpRequestFactoryBuilder(List<Consumer<JdkClientHttpRequestFactory>> customizers) {
super(customizers);
}
@Override
public JdkClientHttpRequestFactoryBuilder withCustomizer(Consumer<JdkClientHttpRequestFactory> customizer) {
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer));
}
@Override
public JdkClientHttpRequestFactoryBuilder withCustomizers(
Collection<Consumer<JdkClientHttpRequestFactory>> customizers) {
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers));
}
@Override
protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
HttpClient httpClient = createHttpClient(settings.connectTimeout(), settings.sslBundle());
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::readTimeout).to(requestFactory::setReadTimeout);
return requestFactory;
}
private HttpClient createHttpClient(Duration connectTimeout, SslBundle sslBundle) {
HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
if (connectTimeout != null) {
httpClientBuilder.connectTimeout(connectTimeout);
}
if (sslBundle != null) {
httpClientBuilder.sslContext(sslBundle.createSslContext());
}
return httpClientBuilder.build();
}
static class Classes {
static final String HTTP_CLIENT = "java.net.http.HttpClient";
static final boolean PRESENT = ClassUtils.isPresent(HTTP_CLIENT, null);
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.util.ClassUtils;
/**
* Builder for {@link ClientHttpRequestFactoryBuilder#jetty()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
* @since 3.4.0
*/
public final class JettyClientHttpRequestFactoryBuilder
extends AbstractClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> {
JettyClientHttpRequestFactoryBuilder() {
this(null);
}
private JettyClientHttpRequestFactoryBuilder(List<Consumer<JettyClientHttpRequestFactory>> customizers) {
super(customizers);
}
@Override
public JettyClientHttpRequestFactoryBuilder withCustomizer(Consumer<JettyClientHttpRequestFactory> customizer) {
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer));
}
@Override
public JettyClientHttpRequestFactoryBuilder withCustomizers(
Collection<Consumer<JettyClientHttpRequestFactory>> customizers) {
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizers));
}
@Override
protected JettyClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
JettyClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
return requestFactory;
}
private static JettyClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
if (sslBundle != null) {
SSLContext sslContext = sslBundle.createSslContext();
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
sslContextFactory.setSslContext(sslContext);
ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(connector));
return new JettyClientHttpRequestFactory(httpClient);
}
return new JettyClientHttpRequestFactory();
}
static class Classes {
static final String HTTP_CLIENT = "org.eclipse.jetty.client.HttpClient";
static final boolean PRESENT = ClassUtils.isPresent(HTTP_CLIENT, null);
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import javax.net.ssl.SSLException;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.SslProvider.SslContextSpec;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslManagerBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.function.ThrowingConsumer;
/**
* Builder for {@link ClientHttpRequestFactoryBuilder#reactor()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
* @since 3.4.0
*/
public final class ReactorClientHttpRequestFactoryBuilder
extends AbstractClientHttpRequestFactoryBuilder<ReactorClientHttpRequestFactory> {
ReactorClientHttpRequestFactoryBuilder() {
this(null);
}
private ReactorClientHttpRequestFactoryBuilder(List<Consumer<ReactorClientHttpRequestFactory>> customizers) {
super(customizers);
}
@Override
public ReactorClientHttpRequestFactoryBuilder withCustomizer(Consumer<ReactorClientHttpRequestFactory> customizer) {
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer));
}
@Override
public ReactorClientHttpRequestFactoryBuilder withCustomizers(
Collection<Consumer<ReactorClientHttpRequestFactory>> customizers) {
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers));
}
@Override
protected ReactorClientHttpRequestFactory createClientHttpRequestFactory(
ClientHttpRequestFactorySettings settings) {
ReactorClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
return requestFactory;
}
private ReactorClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
HttpClient httpClient = HttpClient.create();
httpClient = applyDefaults(httpClient);
if (sslBundle != null) {
httpClient = httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, sslBundle))));
}
return new ReactorClientHttpRequestFactory(httpClient);
}
HttpClient applyDefaults(HttpClient httpClient) {
// Aligns with ReactorClientHttpRequestFactory defaults
return httpClient.compress(true);
}
private void configureSsl(SslContextSpec spec, SslBundle sslBundle) throws SSLException {
SslOptions options = sslBundle.getOptions();
SslManagerBundle managers = sslBundle.getManagers();
SslContextBuilder builder = SslContextBuilder.forClient()
.keyManager(managers.getKeyManagerFactory())
.trustManager(managers.getTrustManagerFactory())
.ciphers(SslOptions.asSet(options.getCiphers()))
.protocols(options.getEnabledProtocols());
spec.sslContext(builder.build());
}
static class Classes {
static final String HTTP_CLIENT = "reactor.netty.http.client.HttpClient";
static final boolean PRESENT = ClassUtils.isPresent(HTTP_CLIENT, null);
}
}

View File

@ -0,0 +1,144 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.function.Supplier;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Internal builder for {@link ClientHttpRequestFactoryBuilder#of(Class)} and
* {@link ClientHttpRequestFactoryBuilder#of(Supplier)}.
*
* @param <T> the {@link ClientHttpRequestFactory} type
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
*/
final class ReflectiveComponentsClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory>
implements ClientHttpRequestFactoryBuilder<T> {
private Supplier<T> requestFactorySupplier;
ReflectiveComponentsClientHttpRequestFactoryBuilder(Supplier<T> requestFactorySupplier) {
Assert.notNull(requestFactorySupplier, "'requestFactorySupplier' must not be null");
this.requestFactorySupplier = requestFactorySupplier;
}
ReflectiveComponentsClientHttpRequestFactoryBuilder(Class<T> requestFactoryType) {
Assert.notNull(requestFactoryType, "'requestFactoryType' must not be null");
this.requestFactorySupplier = () -> createRequestFactory(requestFactoryType);
}
private static <T extends ClientHttpRequestFactory> T createRequestFactory(Class<T> requestFactory) {
try {
Constructor<T> constructor = requestFactory.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@Override
public T build(ClientHttpRequestFactorySettings settings) {
T requestFactory = this.requestFactorySupplier.get();
if (settings != null) {
configure(requestFactory, settings);
}
return requestFactory;
}
private void configure(ClientHttpRequestFactory requestFactory, ClientHttpRequestFactorySettings settings) {
Assert.state(settings.sslBundle() == null, "Unable to set SSL bundler using reflection");
ClientHttpRequestFactory unwrapped = unwrapRequestFactoryIfNecessary(requestFactory);
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::connectTimeout).to((connectTimeout) -> setConnectTimeout(unwrapped, connectTimeout));
map.from(settings::readTimeout).to((readTimeout) -> setReadTimeout(unwrapped, readTimeout));
}
private ClientHttpRequestFactory unwrapRequestFactoryIfNecessary(ClientHttpRequestFactory requestFactory) {
if (!(requestFactory instanceof AbstractClientHttpRequestFactoryWrapper)) {
return requestFactory;
}
Field field = ReflectionUtils.findField(AbstractClientHttpRequestFactoryWrapper.class, "requestFactory");
ReflectionUtils.makeAccessible(field);
ClientHttpRequestFactory unwrappedRequestFactory = requestFactory;
while (unwrappedRequestFactory instanceof AbstractClientHttpRequestFactoryWrapper) {
unwrappedRequestFactory = (ClientHttpRequestFactory) ReflectionUtils.getField(field,
unwrappedRequestFactory);
}
return unwrappedRequestFactory;
}
private void setConnectTimeout(ClientHttpRequestFactory factory, Duration connectTimeout) {
Method method = tryFindMethod(factory, "setConnectTimeout", Duration.class);
if (method != null) {
invoke(factory, method, connectTimeout);
return;
}
method = findMethod(factory, "setConnectTimeout", int.class);
int timeout = Math.toIntExact(connectTimeout.toMillis());
invoke(factory, method, timeout);
}
private void setReadTimeout(ClientHttpRequestFactory factory, Duration readTimeout) {
Method method = tryFindMethod(factory, "setReadTimeout", Duration.class);
if (method != null) {
invoke(factory, method, readTimeout);
return;
}
method = findMethod(factory, "setReadTimeout", int.class);
int timeout = Math.toIntExact(readTimeout.toMillis());
invoke(factory, method, timeout);
}
private Method findMethod(ClientHttpRequestFactory requestFactory, String methodName, Class<?>... parameters) {
Method method = ReflectionUtils.findMethod(requestFactory.getClass(), methodName, parameters);
Assert.state(method != null, () -> "Request factory %s does not have a suitable %s method"
.formatted(requestFactory.getClass().getName(), methodName));
Assert.state(!method.isAnnotationPresent(Deprecated.class),
() -> "Request factory %s has the %s method marked as deprecated"
.formatted(requestFactory.getClass().getName(), methodName));
return method;
}
private Method tryFindMethod(ClientHttpRequestFactory requestFactory, String methodName, Class<?>... parameters) {
Method method = ReflectionUtils.findMethod(requestFactory.getClass(), methodName, parameters);
if (method == null) {
return null;
}
if (method.isAnnotationPresent(Deprecated.class)) {
return null;
}
return method;
}
private void invoke(ClientHttpRequestFactory requestFactory, Method method, Object... parameters) {
ReflectionUtils.invokeMethod(method, requestFactory, parameters);
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
/**
* Builder for {@link ClientHttpRequestFactoryBuilder#simple()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Scott Frederick
* @since 3.4.0
*/
public final class SimpleClientHttpRequestFactoryBuilder
extends AbstractClientHttpRequestFactoryBuilder<SimpleClientHttpRequestFactory> {
SimpleClientHttpRequestFactoryBuilder() {
this(null);
}
private SimpleClientHttpRequestFactoryBuilder(List<Consumer<SimpleClientHttpRequestFactory>> customizers) {
super(customizers);
}
@Override
public SimpleClientHttpRequestFactoryBuilder withCustomizer(Consumer<SimpleClientHttpRequestFactory> customizer) {
return new SimpleClientHttpRequestFactoryBuilder(mergedCustomizers(customizer));
}
@Override
public SimpleClientHttpRequestFactoryBuilder withCustomizers(
Collection<Consumer<SimpleClientHttpRequestFactory>> customizers) {
return new SimpleClientHttpRequestFactoryBuilder(mergedCustomizers(customizers));
}
@Override
protected SimpleClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
SslBundle sslBundle = settings.sslBundle();
SimpleClientHttpRequestFactory requestFactory = (sslBundle != null)
? new SimpleClientHttpsRequestFactory(sslBundle) : new SimpleClientHttpRequestFactory();
Assert.state(sslBundle == null || !sslBundle.getOptions().isSpecified(),
"SSL Options cannot be specified with Java connections");
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
return requestFactory;
}
/**
* {@link SimpleClientHttpsRequestFactory} to configure SSL from an {@link SslBundle}.
*/
private static class SimpleClientHttpsRequestFactory extends SimpleClientHttpRequestFactory {
private final SslBundle sslBundle;
SimpleClientHttpsRequestFactory(SslBundle sslBundle) {
this.sslBundle = sslBundle;
}
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
super.prepareConnection(connection, httpMethod);
if (this.sslBundle != null && connection instanceof HttpsURLConnection secureConnection) {
SSLSocketFactory socketFactory = this.sslBundle.createSslContext().getSocketFactory();
secureConnection.setSSLSocketFactory(socketFactory);
}
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2024 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.
*/
/**
* Client-side HTTP support classes.
*/
package org.springframework.boot.http.client;

View File

@ -18,6 +18,7 @@ package org.springframework.boot.web.client;
import java.time.Duration;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.ClientHttpRequestFactory;
@ -31,7 +32,7 @@ import org.springframework.http.client.ClientHttpRequestFactory;
* @author Phillip Webb
* @author Scott Frederick
* @since 3.0.0
* @see ClientHttpRequestFactories
* @see ClientHttpRequestFactoryBuilder
*/
public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, SslBundle sslBundle) {
@ -74,4 +75,15 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, sslBundle);
}
org.springframework.boot.http.client.ClientHttpRequestFactorySettings adapt() {
return new org.springframework.boot.http.client.ClientHttpRequestFactorySettings(connectTimeout(),
readTimeout(), sslBundle());
}
static ClientHttpRequestFactorySettings of(
org.springframework.boot.http.client.ClientHttpRequestFactorySettings settings) {
return new ClientHttpRequestFactorySettings(settings.connectTimeout(), settings.readTimeout(),
settings.sslBundle());
}
}

View File

@ -5,11 +5,11 @@ org.springframework.boot.WebApplicationType.WebApplicationTypeRuntimeHints,\
org.springframework.boot.context.config.ConfigDataLocationRuntimeHints,\
org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\
org.springframework.boot.env.PropertySourceRuntimeHints,\
org.springframework.boot.http.client.ClientHttpRequestFactoryRuntimeHints,\
org.springframework.boot.jdbc.DataSourceBuilderRuntimeHints,\
org.springframework.boot.json.JacksonRuntimeHints,\
org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\
org.springframework.boot.logging.logback.LogbackRuntimeHints,\
org.springframework.boot.web.client.ClientHttpRequestFactoriesRuntimeHints,\
org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServerRuntimeHints,\
org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints

View File

@ -0,0 +1,151 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import javax.net.ssl.SSLHandshakeException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.Ssl.ClientAuth;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Base class for {@link ClientHttpRequestFactoryBuilder} tests.
*
* @param <T> The {@link ClientHttpRequestFactory} type
* @author Phillip Webb
* @author Andy Wilkinson
*/
@DirtiesUrlFactories
abstract class AbstractClientHttpRequestFactoryBuilderTests<T extends ClientHttpRequestFactory> {
private final Class<T> requestFactoryType;
private final ClientHttpRequestFactoryBuilder<T> builder;
AbstractClientHttpRequestFactoryBuilderTests(Class<T> requestFactoryType,
ClientHttpRequestFactoryBuilder<T> builder) {
this.requestFactoryType = requestFactoryType;
this.builder = builder;
}
@Test
void buildReturnsRequestFactoryOfExpectedType() {
T requestFactory = this.builder.build();
assertThat(requestFactory).isInstanceOf(this.requestFactoryType);
}
@Test
void buildWhenHasConnectTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(60));
T requestFactory = this.builder.build(settings);
assertThat(connectTimeout(requestFactory)).isEqualTo(Duration.ofSeconds(60).toMillis());
}
@Test
void buildWhenHadReadTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withReadTimeout(Duration.ofSeconds(120));
T requestFactory = this.builder.build(settings);
assertThat(readTimeout(requestFactory)).isEqualTo(Duration.ofSeconds(120).toMillis());
}
@ParameterizedTest
@ValueSource(strings = { "GET", "POST" })
void connectWithSslBundle(String httpMethod) throws Exception {
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
webServerFactory.setSsl(ssl());
WebServer webServer = webServerFactory
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
try {
webServer.start();
int port = webServer.getPort();
URI uri = new URI("https://localhost:%s".formatted(port));
ClientHttpRequestFactory insecureRequestFactory = this.builder.build();
ClientHttpRequest insecureRequest = request(insecureRequestFactory, uri, httpMethod);
assertThatExceptionOfType(SSLHandshakeException.class)
.isThrownBy(() -> insecureRequest.execute().getBody());
ClientHttpRequestFactory secureRequestFactory = this.builder
.build(ClientHttpRequestFactorySettings.ofSslBundle(sslBundle()));
ClientHttpRequest secureRequest = request(secureRequestFactory, uri, httpMethod);
String secureResponse = StreamUtils.copyToString(secureRequest.execute().getBody(), StandardCharsets.UTF_8);
assertThat(secureResponse).contains("Received " + httpMethod + " request to /");
}
finally {
webServer.stop();
}
}
private ClientHttpRequest request(ClientHttpRequestFactory factory, URI uri, String method) throws IOException {
return factory.createRequest(uri, HttpMethod.valueOf(method));
}
private Ssl ssl() {
Ssl ssl = new Ssl();
ssl.setClientAuth(ClientAuth.NEED);
ssl.setKeyPassword("password");
ssl.setKeyStore("classpath:test.jks");
ssl.setTrustStore("classpath:test.jks");
return ssl;
}
protected final SslBundle sslBundle() {
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks");
JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails);
return SslBundle.of(stores, SslBundleKey.of("password"));
}
protected abstract long connectTimeout(T requestFactory);
protected abstract long readTimeout(T requestFactory);
public static class TestServlet extends HttpServlet {
@Override
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
res.getWriter().println("Received " + req.getMethod() + " request to " + req.getRequestURI());
}
}
}

View File

@ -0,0 +1,181 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
*/
class ClientHttpRequestFactoryBuilderTests {
@Test
void withCustomizerAppliesCustomizers() {
ClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> builder = (
settings) -> new JettyClientHttpRequestFactory();
builder = builder.withCustomizer(this::setJettyReadTimeout);
JettyClientHttpRequestFactory factory = builder.build(null);
assertThat(factory).extracting("readTimeout").isEqualTo(5000L);
}
@Test
void withCustomizersAppliesCustomizers() {
ClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> builder = (
settings) -> new JettyClientHttpRequestFactory();
builder = builder.withCustomizers(List.of(this::setJettyReadTimeout));
JettyClientHttpRequestFactory factory = builder.build(null);
assertThat(factory).extracting("readTimeout").isEqualTo(5000L);
}
@Test
void httpComponentsReturnsHttpComponentsFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.httpComponents())
.isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
}
@Test
void jettyReturnsJettyFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.jetty()).isInstanceOf(JettyClientHttpRequestFactoryBuilder.class);
}
@Test
void reactorReturnsReactorFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.reactor())
.isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class);
}
@Test
void jdkReturnsJdkFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.jdk()).isInstanceOf(JdkClientHttpRequestFactoryBuilder.class);
}
@Test
void simpleReturnsSimpleFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.simple()).isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class);
}
@Test
void ofWhenExactlyClientHttpRequestFactoryTypeThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ClientHttpRequestFactoryBuilder.of(ClientHttpRequestFactory.class))
.withMessage("'requestFactoryType' must be an implementation of ClientHttpRequestFactory");
}
@Test
void ofWhenSimpleFactoryReturnsSimpleFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.of(SimpleClientHttpRequestFactory.class))
.isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class);
}
@Test
void ofWhenHttpComponentsFactoryReturnsHttpComponentsFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.of(HttpComponentsClientHttpRequestFactory.class))
.isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
}
@Test
void ofWhenReactorFactoryReturnsReactorFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.of(ReactorClientHttpRequestFactory.class))
.isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class);
}
@Test
void ofWhenJdkFactoryReturnsJdkFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.of(JdkClientHttpRequestFactory.class))
.isInstanceOf(JdkClientHttpRequestFactoryBuilder.class);
}
@Test
void ofWhenUnknownTypeReturnsReflectiveFactoryBuilder() {
ClientHttpRequestFactoryBuilder<TestClientHttpRequestFactory> builder = ClientHttpRequestFactoryBuilder
.of(TestClientHttpRequestFactory.class);
assertThat(builder).isInstanceOf(ReflectiveComponentsClientHttpRequestFactoryBuilder.class);
assertThat(builder.build(null)).isInstanceOf(TestClientHttpRequestFactory.class);
}
@Test
void ofWithSupplierWhenSupplierIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ClientHttpRequestFactoryBuilder.of((Supplier<ClientHttpRequestFactory>) null))
.withMessage("'requestFactorySupplier' must not be null");
}
@Test
void ofWithSupplierReturnsReflectiveFactoryBuilder() {
assertThat(ClientHttpRequestFactoryBuilder.of(SimpleClientHttpRequestFactory::new))
.isInstanceOf(ReflectiveComponentsClientHttpRequestFactoryBuilder.class);
}
@Test
void detectWhenHttpComponents() {
assertThat(ClientHttpRequestFactoryBuilder.detect())
.isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
}
@Test
@ClassPathExclusions("httpclient5-*.jar")
void detectWhenJetty() {
assertThat(ClientHttpRequestFactoryBuilder.detect()).isInstanceOf(JettyClientHttpRequestFactoryBuilder.class);
}
@Test
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
void detectWhenReactor() {
assertThat(ClientHttpRequestFactoryBuilder.detect()).isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class);
}
@Test
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "reactor-netty-http-*.jar" })
void detectWhenJdk() {
assertThat(ClientHttpRequestFactoryBuilder.detect()).isInstanceOf(JdkClientHttpRequestFactoryBuilder.class);
}
private void setJettyReadTimeout(JettyClientHttpRequestFactory factory) {
factory.setReadTimeout(Duration.ofSeconds(5));
}
public static class TestClientHttpRequestFactory implements ClientHttpRequestFactory {
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
throw new UnsupportedOperationException();
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.web.client;
package org.springframework.boot.http.client;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@ -34,17 +34,17 @@ import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ClientHttpRequestFactoriesRuntimeHints}.
* Tests for {@link ClientHttpRequestFactoryRuntimeHints}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
class ClientHttpRequestFactoriesRuntimeHintsTests {
class ClientHttpRequestFactoryRuntimeHintsTests {
@Test
void shouldRegisterHints() {
RuntimeHints hints = new RuntimeHints();
new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader());
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
Field requestFactoryField = ReflectionUtils.findField(AbstractClientHttpRequestFactoryWrapper.class,
"requestFactory");
@ -55,7 +55,7 @@ class ClientHttpRequestFactoriesRuntimeHintsTests {
@Test
void shouldRegisterHttpComponentHints() {
RuntimeHints hints = new RuntimeHints();
new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader());
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
assertThat(reflection
.onMethod(method(HttpComponentsClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
@ -65,7 +65,7 @@ class ClientHttpRequestFactoriesRuntimeHintsTests {
@Test
void shouldRegisterJettyClientHints() {
RuntimeHints hints = new RuntimeHints();
new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader());
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
assertThat(reflection.onMethod(method(JettyClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
.accepts(hints);
@ -76,7 +76,7 @@ class ClientHttpRequestFactoriesRuntimeHintsTests {
@Test
void shouldRegisterReactorHints() {
RuntimeHints hints = new RuntimeHints();
new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader());
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
assertThat(reflection.onMethod(method(ReactorClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
.accepts(hints);
@ -87,7 +87,7 @@ class ClientHttpRequestFactoriesRuntimeHintsTests {
@Test
void shouldRegisterSimpleHttpHints() {
RuntimeHints hints = new RuntimeHints();
new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader());
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
assertThat(reflection.onMethod(method(SimpleClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
.accepts(hints);

View File

@ -0,0 +1,73 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ssl.SslBundle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ClientHttpRequestFactorySettings}.
*
* @author Phillip Webb
*/
class ClientHttpRequestFactorySettingsTests {
private static final Duration ONE_SECOND = Duration.ofSeconds(1);
@Test
void defaultsHasNullValues() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults();
assertThat(settings.connectTimeout()).isNull();
assertThat(settings.readTimeout()).isNull();
assertThat(settings.sslBundle()).isNull();
}
@Test
void withConnectTimeoutReturnsInstanceWithUpdatedConnectionTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(ONE_SECOND);
assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND);
assertThat(settings.readTimeout()).isNull();
assertThat(settings.sslBundle()).isNull();
}
@Test
void withReadTimeoutReturnsInstanceWithUpdatedReadTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withReadTimeout(ONE_SECOND);
assertThat(settings.connectTimeout()).isNull();
assertThat(settings.readTimeout()).isEqualTo(ONE_SECOND);
assertThat(settings.sslBundle()).isNull();
}
@Test
void withSslBundleReturnsInstanceWithUpdatedSslBundle() {
SslBundle sslBundle = mock(SslBundle.class);
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withSslBundle(sslBundle);
assertThat(settings.connectTimeout()).isNull();
assertThat(settings.readTimeout()).isNull();
assertThat(settings.sslBundle()).isSameAs(sslBundle);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.core5.function.Resolver;
import org.apache.hc.core5.http.io.SocketConfig;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class HttpComponentsClientHttpRequestFactoryBuilderTests
extends AbstractClientHttpRequestFactoryBuilderTests<HttpComponentsClientHttpRequestFactory> {
HttpComponentsClientHttpRequestFactoryBuilderTests() {
super(HttpComponentsClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.httpComponents());
}
@Override
protected long connectTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
return (long) ReflectionTestUtils.getField(requestFactory, "connectTimeout");
}
@Override
@SuppressWarnings("unchecked")
protected long readTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
HttpClient httpClient = requestFactory.getHttpClient();
Object connectionManager = ReflectionTestUtils.getField(httpClient, "connManager");
SocketConfig socketConfig = ((Resolver<HttpRoute, SocketConfig>) ReflectionTestUtils.getField(connectionManager,
"socketConfigResolver"))
.resolve(null);
return socketConfig.getSoTimeout().toMilliseconds();
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link JdkClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
*/
class JdkClientHttpRequestFactoryBuilderTests
extends AbstractClientHttpRequestFactoryBuilderTests<JdkClientHttpRequestFactory> {
JdkClientHttpRequestFactoryBuilderTests() {
super(JdkClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.jdk());
}
@Override
protected long connectTimeout(JdkClientHttpRequestFactory requestFactory) {
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient");
return httpClient.connectTimeout().get().toMillis();
}
@Override
protected long readTimeout(JdkClientHttpRequestFactory requestFactory) {
Duration readTimeout = (Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout");
return readTimeout.toMillis();
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import org.eclipse.jetty.client.HttpClient;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link JettyClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
*/
class JettyClientHttpRequestFactoryBuilderTests
extends AbstractClientHttpRequestFactoryBuilderTests<JettyClientHttpRequestFactory> {
JettyClientHttpRequestFactoryBuilderTests() {
super(JettyClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.jetty());
}
@Override
protected long connectTimeout(JettyClientHttpRequestFactory requestFactory) {
return ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).getConnectTimeout();
}
@Override
protected long readTimeout(JettyClientHttpRequestFactory requestFactory) {
return (long) ReflectionTestUtils.getField(requestFactory, "readTimeout");
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.time.Duration;
import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link ReactorClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class ReactorClientHttpRequestFactoryBuilderTests
extends AbstractClientHttpRequestFactoryBuilderTests<ReactorClientHttpRequestFactory> {
ReactorClientHttpRequestFactoryBuilderTests() {
super(ReactorClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.reactor());
}
@Override
protected long connectTimeout(ReactorClientHttpRequestFactory requestFactory) {
return (int) ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).configuration()
.options()
.get(ChannelOption.CONNECT_TIMEOUT_MILLIS);
}
@Override
protected long readTimeout(ReactorClientHttpRequestFactory requestFactory) {
return ((Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout")).toMillis();
}
}

View File

@ -0,0 +1,252 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import java.net.URI;
import java.time.Duration;
import org.eclipse.jetty.client.HttpClient;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ReflectiveComponentsClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
*/
class ReflectiveComponentsClientHttpRequestFactoryBuilderTests
extends AbstractClientHttpRequestFactoryBuilderTests<ClientHttpRequestFactory> {
ReflectiveComponentsClientHttpRequestFactoryBuilderTests() {
super(ClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.of(JettyClientHttpRequestFactory::new));
}
@Override
void connectWithSslBundle(String httpMethod) throws Exception {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.ofSslBundle(sslBundle());
assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings))
.withMessage("Unable to set SSL bundler using reflection");
}
@Test
void buildWithClassCreatesFactory() {
assertThat(ofTestRequestFactory().build()).isInstanceOf(TestClientHttpRequestFactory.class);
}
@Test
void buildWithClassWhenHasConnectTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(60));
TestClientHttpRequestFactory requestFactory = ofTestRequestFactory().build(settings);
assertThat(requestFactory.connectTimeout).isEqualTo(Duration.ofSeconds(60).toMillis());
}
@Test
void buildWithClassWhenHasReadTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withReadTimeout(Duration.ofSeconds(90));
TestClientHttpRequestFactory requestFactory = ofTestRequestFactory().build(settings);
assertThat(requestFactory.readTimeout).isEqualTo(Duration.ofSeconds(90).toMillis());
}
@Test
void buildWithClassWhenUnconfigurableTypeWithConnectTimeoutThrowsException() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(60));
assertThatIllegalStateException().isThrownBy(() -> ofUnconfigurableRequestFactory().build(settings))
.withMessageContaining("suitable setConnectTimeout method");
}
@Test
void buildWithClassWhenUnconfigurableTypeWithReadTimeoutThrowsException() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withReadTimeout(Duration.ofSeconds(60));
assertThatIllegalStateException().isThrownBy(() -> ofUnconfigurableRequestFactory().build(settings))
.withMessageContaining("suitable setReadTimeout method");
}
@Test
void buildWithClassWhenDeprecatedMethodsTypeWithConnectTimeoutThrowsException() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(60));
assertThatIllegalStateException().isThrownBy(() -> ofDeprecatedMethodsRequestFactory().build(settings))
.withMessageContaining("setConnectTimeout method marked as deprecated");
}
@Test
void buildWithClassWhenDeprecatedMethodsTypeWithReadTimeoutThrowsException() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withReadTimeout(Duration.ofSeconds(60));
assertThatIllegalStateException().isThrownBy(() -> ofDeprecatedMethodsRequestFactory().build(settings))
.withMessageContaining("setReadTimeout method marked as deprecated");
}
@Test
void buildWithSupplierWhenWrappedRequestFactoryTypeWithConnectTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofMillis(1234));
SimpleClientHttpRequestFactory wrappedRequestFactory = new SimpleClientHttpRequestFactory();
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
.of(() -> new BufferingClientHttpRequestFactory(wrappedRequestFactory))
.build(settings);
assertThat(requestFactory).extracting("requestFactory").isSameAs(wrappedRequestFactory);
assertThat(wrappedRequestFactory).hasFieldOrPropertyWithValue("connectTimeout", 1234);
}
@Test
void buildWithSupplierWhenWrappedRequestFactoryTypeWithReadTimeout() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withReadTimeout(Duration.ofMillis(1234));
SimpleClientHttpRequestFactory wrappedRequestFactory = new SimpleClientHttpRequestFactory();
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
.of(() -> new BufferingClientHttpRequestFactory(wrappedRequestFactory))
.build(settings);
assertThat(requestFactory).extracting("requestFactory").isSameAs(wrappedRequestFactory);
assertThat(wrappedRequestFactory).hasFieldOrPropertyWithValue("readTimeout", 1234);
}
@Test
void buildWithClassWhenHasMultipleTimeoutSettersFavorsDurationMethods() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(1))
.withReadTimeout(Duration.ofSeconds(2));
IntAndDurationTimeoutsClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
.of(IntAndDurationTimeoutsClientHttpRequestFactory.class)
.build(settings);
assertThat((requestFactory).connectTimeout).isZero();
assertThat((requestFactory).readTimeout).isZero();
assertThat((requestFactory).connectTimeoutDuration).isEqualTo(Duration.ofSeconds(1));
assertThat((requestFactory).readTimeoutDuration).isEqualTo(Duration.ofSeconds(2));
}
private ClientHttpRequestFactoryBuilder<TestClientHttpRequestFactory> ofTestRequestFactory() {
return ClientHttpRequestFactoryBuilder.of(TestClientHttpRequestFactory.class);
}
private ClientHttpRequestFactoryBuilder<UnconfigurableClientHttpRequestFactory> ofUnconfigurableRequestFactory() {
return ClientHttpRequestFactoryBuilder.of(UnconfigurableClientHttpRequestFactory.class);
}
private ClientHttpRequestFactoryBuilder<DeprecatedMethodsClientHttpRequestFactory> ofDeprecatedMethodsRequestFactory() {
return ClientHttpRequestFactoryBuilder.of(DeprecatedMethodsClientHttpRequestFactory.class);
}
@Override
protected long connectTimeout(ClientHttpRequestFactory requestFactory) {
return ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).getConnectTimeout();
}
@Override
protected long readTimeout(ClientHttpRequestFactory requestFactory) {
return (long) ReflectionTestUtils.getField(requestFactory, "readTimeout");
}
public static class TestClientHttpRequestFactory implements ClientHttpRequestFactory {
private int connectTimeout;
private int readTimeout;
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
throw new UnsupportedOperationException();
}
public void setConnectTimeout(int timeout) {
this.connectTimeout = timeout;
}
public void setReadTimeout(int timeout) {
this.readTimeout = timeout;
}
}
public static class UnconfigurableClientHttpRequestFactory implements ClientHttpRequestFactory {
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
throw new UnsupportedOperationException();
}
}
public static class DeprecatedMethodsClientHttpRequestFactory implements ClientHttpRequestFactory {
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
throw new UnsupportedOperationException();
}
@Deprecated(since = "3.0.0", forRemoval = false)
public void setConnectTimeout(int timeout) {
}
@Deprecated(since = "3.0.0", forRemoval = false)
public void setReadTimeout(int timeout) {
}
@Deprecated(since = "3.0.0", forRemoval = false)
public void setBufferRequestBody(boolean bufferRequestBody) {
}
}
public static class IntAndDurationTimeoutsClientHttpRequestFactory implements ClientHttpRequestFactory {
private int readTimeout;
private int connectTimeout;
private Duration readTimeoutDuration;
private Duration connectTimeoutDuration;
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
throw new UnsupportedOperationException();
}
public void setConnectTimeout(int timeout) {
this.connectTimeout = timeout;
}
public void setReadTimeout(int timeout) {
this.readTimeout = timeout;
}
public void setConnectTimeout(Duration timeout) {
this.connectTimeoutDuration = timeout;
}
public void setReadTimeout(Duration timeout) {
this.readTimeoutDuration = timeout;
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2012-2024 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.boot.http.client;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link SimpleClientHttpRequestFactoryBuilder}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class SimpleClientHttpRequestFactoryBuilderTests
extends AbstractClientHttpRequestFactoryBuilderTests<SimpleClientHttpRequestFactory> {
SimpleClientHttpRequestFactoryBuilderTests() {
super(SimpleClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.simple());
}
@Override
protected long connectTimeout(SimpleClientHttpRequestFactory requestFactory) {
return (int) ReflectionTestUtils.getField(requestFactory, "connectTimeout");
}
@Override
protected long readTimeout(SimpleClientHttpRequestFactory requestFactory) {
return (int) ReflectionTestUtils.getField(requestFactory, "readTimeout");
}
}