diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..135b8a45879 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java @@ -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 the {@link ClientHttpRequestFactory} type + * @author Phillip Webb + */ +abstract class AbstractClientHttpRequestFactoryBuilder + implements ClientHttpRequestFactoryBuilder { + + private final List> customizers; + + protected AbstractClientHttpRequestFactoryBuilder(List> customizers) { + this.customizers = (customizers != null) ? customizers : Collections.emptyList(); + } + + protected final List> mergedCustomizers(Consumer customizer) { + Assert.notNull(this.customizers, "'customizer' must not be null"); + return merge(this.customizers, List.of(customizer)); + } + + protected final List> mergedCustomizers(Collection> customizers) { + Assert.notNull(customizers, "'customizers' must not be null"); + Assert.noNullElements(customizers, "'customizers' must not contain null elements"); + return merge(this.customizers, customizers); + } + + private List merge(Collection list, Collection additional) { + List 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); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..1711ae9399a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilder.java @@ -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 the {@link ClientHttpRequestFactory} type + * @author Phillip Webb + * @since 3.4.0 + */ +@FunctionalInterface +public interface ClientHttpRequestFactoryBuilder { + + /** + * 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 withCustomizer(Consumer 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 withCustomizers(Collection> 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: + *
    + *
  • {@link HttpComponentsClientHttpRequestFactory}
  • + *
  • {@link JdkClientHttpRequestFactory}
  • + *
  • {@link JettyClientHttpRequestFactory}
  • + *
  • {@link ReactorClientHttpRequestFactory}
  • + *
  • {@link SimpleClientHttpRequestFactory}
  • + *
+ * @param the {@link ClientHttpRequestFactory} type + * @param requestFactoryType the {@link ClientHttpRequestFactory} type + * @return a new {@link ClientHttpRequestFactoryBuilder} + */ + @SuppressWarnings("unchecked") + static ClientHttpRequestFactoryBuilder of(Class 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) httpComponents(); + } + if (requestFactoryType == JettyClientHttpRequestFactory.class) { + return (ClientHttpRequestFactoryBuilder) jetty(); + } + if (requestFactoryType == ReactorClientHttpRequestFactory.class) { + return (ClientHttpRequestFactoryBuilder) reactor(); + } + if (requestFactoryType == JdkClientHttpRequestFactory.class) { + return (ClientHttpRequestFactoryBuilder) jdk(); + } + if (requestFactoryType == SimpleClientHttpRequestFactory.class) { + return (ClientHttpRequestFactoryBuilder) simple(); + } + return new ReflectiveComponentsClientHttpRequestFactoryBuilder<>(requestFactoryType); + } + + /** + * Return a new {@link ClientHttpRequestFactoryBuilder} from the given supplier, using + * reflection to ultimately apply the {@link ClientHttpRequestFactorySettings}. + * @param the {@link ClientHttpRequestFactory} type + * @param requestFactorySupplier the {@link ClientHttpRequestFactory} supplier + * @return a new {@link ClientHttpRequestFactoryBuilder} + */ + static ClientHttpRequestFactoryBuilder of( + Supplier requestFactorySupplier) { + return new ReflectiveComponentsClientHttpRequestFactoryBuilder<>(requestFactorySupplier); + } + + /** + * Detect the most suitable {@link ClientHttpRequestFactoryBuilder} based on the + * classpath. The methods favors builders in the following order: + *
    + *
  1. {@link #httpComponents()}
  2. + *
  3. {@link #jetty()}
  4. + *
  5. {@link #reactor()}
  6. + *
  7. {@link #jdk()}
  8. + *
  9. {@link #simple()}
  10. + *
+ * @return the most suitable {@link ClientHttpRequestFactoryBuilder} for the classpath + */ + static ClientHttpRequestFactoryBuilder 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(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactoryRuntimeHints.java similarity index 71% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactoryRuntimeHints.java index 9f243c4a9b6..a1451a0eefa 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactoryRuntimeHints.java @@ -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(); }); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettings.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettings.java new file mode 100644 index 00000000000..43180c871d2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettings.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..cb8efd0a57c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java @@ -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 { + + HttpComponentsClientHttpRequestFactoryBuilder() { + this(Collections.emptyList()); + } + + private HttpComponentsClientHttpRequestFactoryBuilder( + List> customizers) { + super(customizers); + } + + @Override + public HttpComponentsClientHttpRequestFactoryBuilder withCustomizer( + Consumer customizer) { + return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + } + + @Override + public HttpComponentsClientHttpRequestFactoryBuilder withCustomizers( + Collection> 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); + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..2847dc384ec --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java @@ -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 { + + JdkClientHttpRequestFactoryBuilder() { + this(null); + } + + private JdkClientHttpRequestFactoryBuilder(List> customizers) { + super(customizers); + } + + @Override + public JdkClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { + return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + } + + @Override + public JdkClientHttpRequestFactoryBuilder withCustomizers( + Collection> 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); + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..0363656e7e6 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java @@ -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 { + + JettyClientHttpRequestFactoryBuilder() { + this(null); + } + + private JettyClientHttpRequestFactoryBuilder(List> customizers) { + super(customizers); + } + + @Override + public JettyClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { + return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + } + + @Override + public JettyClientHttpRequestFactoryBuilder withCustomizers( + Collection> 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); + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..1c45655fa62 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java @@ -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 { + + ReactorClientHttpRequestFactoryBuilder() { + this(null); + } + + private ReactorClientHttpRequestFactoryBuilder(List> customizers) { + super(customizers); + } + + @Override + public ReactorClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { + return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + } + + @Override + public ReactorClientHttpRequestFactoryBuilder withCustomizers( + Collection> 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); + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..3693e23f444 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilder.java @@ -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 the {@link ClientHttpRequestFactory} type + * @author Phillip Webb + * @author Andy Wilkinson + * @author Scott Frederick + */ +final class ReflectiveComponentsClientHttpRequestFactoryBuilder + implements ClientHttpRequestFactoryBuilder { + + private Supplier requestFactorySupplier; + + ReflectiveComponentsClientHttpRequestFactoryBuilder(Supplier requestFactorySupplier) { + Assert.notNull(requestFactorySupplier, "'requestFactorySupplier' must not be null"); + this.requestFactorySupplier = requestFactorySupplier; + } + + ReflectiveComponentsClientHttpRequestFactoryBuilder(Class requestFactoryType) { + Assert.notNull(requestFactoryType, "'requestFactoryType' must not be null"); + this.requestFactorySupplier = () -> createRequestFactory(requestFactoryType); + } + + private static T createRequestFactory(Class requestFactory) { + try { + Constructor 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); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java new file mode 100644 index 00000000000..6d92620be83 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilder.java @@ -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 { + + SimpleClientHttpRequestFactoryBuilder() { + this(null); + } + + private SimpleClientHttpRequestFactoryBuilder(List> customizers) { + super(customizers); + } + + @Override + public SimpleClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { + return new SimpleClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + } + + @Override + public SimpleClientHttpRequestFactoryBuilder withCustomizers( + Collection> 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); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/package-info.java new file mode 100644 index 00000000000..95d81ef006d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java index f7dd4af2de8..f8c851b89d4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java @@ -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()); + } + } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index 451a2ae4b49..e439a91ea69 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..bc44711c9a3 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java @@ -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 The {@link ClientHttpRequestFactory} type + * @author Phillip Webb + * @author Andy Wilkinson + */ +@DirtiesUrlFactories +abstract class AbstractClientHttpRequestFactoryBuilderTests { + + private final Class requestFactoryType; + + private final ClientHttpRequestFactoryBuilder builder; + + AbstractClientHttpRequestFactoryBuilderTests(Class requestFactoryType, + ClientHttpRequestFactoryBuilder 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()); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..c777ff59a04 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactoryBuilderTests.java @@ -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 builder = ( + settings) -> new JettyClientHttpRequestFactory(); + builder = builder.withCustomizer(this::setJettyReadTimeout); + JettyClientHttpRequestFactory factory = builder.build(null); + assertThat(factory).extracting("readTimeout").isEqualTo(5000L); + } + + @Test + void withCustomizersAppliesCustomizers() { + ClientHttpRequestFactoryBuilder 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 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) 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(); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactoryRuntimeHintsTests.java similarity index 85% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactoryRuntimeHintsTests.java index 5ed87b2d882..a6918df4814 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactoryRuntimeHintsTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettingsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettingsTests.java new file mode 100644 index 00000000000..33270263e3c --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ClientHttpRequestFactorySettingsTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..36f60c9e42a --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java @@ -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 { + + 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) ReflectionTestUtils.getField(connectionManager, + "socketConfigResolver")) + .resolve(null); + return socketConfig.getSoTimeout().toMilliseconds(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..edc8b2258dc --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java @@ -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 { + + 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(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..627eaf2776d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java @@ -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 { + + 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"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..81e624e6085 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java @@ -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 { + + 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(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..ed55cc83d5e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java @@ -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 { + + 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 ofTestRequestFactory() { + return ClientHttpRequestFactoryBuilder.of(TestClientHttpRequestFactory.class); + } + + private ClientHttpRequestFactoryBuilder ofUnconfigurableRequestFactory() { + return ClientHttpRequestFactoryBuilder.of(UnconfigurableClientHttpRequestFactory.class); + } + + private ClientHttpRequestFactoryBuilder 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java new file mode 100644 index 00000000000..ae92de4a2d0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java @@ -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 { + + 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"); + } + +}