Rework HTTP client modules
This commit is contained in:
committed by
Phillip Webb
parent
4763ef2463
commit
7a9be5bd4a
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Internal base class used for {@link ClientHttpRequestFactoryBuilder} implementations.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpRequestFactory} type
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
abstract class AbstractClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory>
|
||||
implements ClientHttpRequestFactoryBuilder<T> {
|
||||
|
||||
private final List<Consumer<T>> customizers;
|
||||
|
||||
protected AbstractClientHttpRequestFactoryBuilder(List<Consumer<T>> customizers) {
|
||||
this.customizers = (customizers != null) ? customizers : Collections.emptyList();
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> getCustomizers() {
|
||||
return this.customizers;
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> mergedCustomizers(Consumer<T> customizer) {
|
||||
Assert.notNull(this.customizers, "'customizer' must not be null");
|
||||
return merge(this.customizers, List.of(customizer));
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> mergedCustomizers(Collection<Consumer<T>> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
|
||||
return merge(this.customizers, customizers);
|
||||
}
|
||||
|
||||
private <E> List<E> merge(Collection<E> list, Collection<? extends E> additional) {
|
||||
List<E> merged = new ArrayList<>(list);
|
||||
merged.addAll(additional);
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final T build(ClientHttpRequestFactorySettings settings) {
|
||||
T factory = createClientHttpRequestFactory(
|
||||
(settings != null) ? settings : ClientHttpRequestFactorySettings.defaults());
|
||||
LambdaSafe.callbacks(Consumer.class, this.customizers, factory).invoke((consumer) -> consumer.accept(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
protected abstract T createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings);
|
||||
|
||||
protected final HttpClientSettings asHttpClientSettings(ClientHttpRequestFactorySettings settings) {
|
||||
return (settings != null) ? new HttpClientSettings(settings.redirects(), settings.connectTimeout(),
|
||||
settings.readTimeout(), settings.sslBundle()) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Interface used to build a fully configured {@link ClientHttpRequestFactory}. Builders
|
||||
* for {@link #httpComponents() Apache HTTP Components}, {@link #jetty() Jetty},
|
||||
* {@link #reactor() Reactor}, {@link #jdk() JDK} and {@link #simple() simple client} can
|
||||
* be obtained using the factory methods on this interface. The {@link #of(Class)} and
|
||||
* {@link #of(Supplier)} methods may be used to instantiate other
|
||||
* {@link ClientHttpRequestFactory} instances using reflection.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpRequestFactory} type
|
||||
* @author Phillip Webb
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory> {
|
||||
|
||||
/**
|
||||
* Build a default configured {@link ClientHttpRequestFactory}.
|
||||
* @return a default configured {@link ClientHttpRequestFactory}.
|
||||
*/
|
||||
default T build() {
|
||||
return build(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully configured {@link ClientHttpRequestFactory}, applying the given
|
||||
* {@code settings} if they are provided.
|
||||
* @param settings the settings to apply or {@code null}
|
||||
* @return a fully configured {@link ClientHttpRequestFactory}.
|
||||
*/
|
||||
T build(ClientHttpRequestFactorySettings settings);
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactoryBuilder} that applies the given
|
||||
* customizer to the {@link ClientHttpRequestFactory} after it has been built.
|
||||
* @param customizer the customizers to apply
|
||||
* @return a new {@link ClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
default ClientHttpRequestFactoryBuilder<T> withCustomizer(Consumer<T> customizer) {
|
||||
return withCustomizers(List.of(customizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactoryBuilder} that applies the given
|
||||
* customizers to the {@link ClientHttpRequestFactory} after it has been built.
|
||||
* @param customizers the customizers to apply
|
||||
* @return a new {@link ClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default ClientHttpRequestFactoryBuilder<T> withCustomizers(Collection<Consumer<T>> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
|
||||
return (settings) -> {
|
||||
T factory = build(settings);
|
||||
LambdaSafe.callbacks(Consumer.class, customizers, factory).invoke((consumer) -> consumer.accept(factory));
|
||||
return factory;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link HttpComponentsClientHttpRequestFactoryBuilder} that can be used to
|
||||
* build a {@link HttpComponentsClientHttpRequestFactory}.
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
static HttpComponentsClientHttpRequestFactoryBuilder httpComponents() {
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link JettyClientHttpRequestFactoryBuilder} that can be used to build a
|
||||
* {@link JettyClientHttpRequestFactory}.
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
static JettyClientHttpRequestFactoryBuilder jetty() {
|
||||
return new JettyClientHttpRequestFactoryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ReactorClientHttpRequestFactoryBuilder} that can be used to build a
|
||||
* {@link ReactorClientHttpRequestFactory}.
|
||||
* @return a new {@link ReactorClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
static ReactorClientHttpRequestFactoryBuilder reactor() {
|
||||
return new ReactorClientHttpRequestFactoryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link JdkClientHttpRequestFactoryBuilder} that can be used to build a
|
||||
* {@link JdkClientHttpRequestFactory} .
|
||||
* @return a new {@link JdkClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
static JdkClientHttpRequestFactoryBuilder jdk() {
|
||||
return new JdkClientHttpRequestFactoryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link SimpleClientHttpRequestFactoryBuilder} that can be used to build a
|
||||
* {@link SimpleClientHttpRequestFactory} .
|
||||
* @return a new {@link SimpleClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
static SimpleClientHttpRequestFactoryBuilder simple() {
|
||||
return new SimpleClientHttpRequestFactoryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactoryBuilder} for the given
|
||||
* {@code requestFactoryType}. The following implementations are supported without the
|
||||
* use of reflection:
|
||||
* <ul>
|
||||
* <li>{@link HttpComponentsClientHttpRequestFactory}</li>
|
||||
* <li>{@link JdkClientHttpRequestFactory}</li>
|
||||
* <li>{@link JettyClientHttpRequestFactory}</li>
|
||||
* <li>{@link ReactorClientHttpRequestFactory}</li>
|
||||
* <li>{@link SimpleClientHttpRequestFactory}</li>
|
||||
* </ul>
|
||||
* @param <T> the {@link ClientHttpRequestFactory} type
|
||||
* @param requestFactoryType the {@link ClientHttpRequestFactory} type
|
||||
* @return a new {@link ClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ClientHttpRequestFactory> ClientHttpRequestFactoryBuilder<T> of(Class<T> requestFactoryType) {
|
||||
Assert.notNull(requestFactoryType, "'requestFactoryType' must not be null");
|
||||
Assert.isTrue(requestFactoryType != ClientHttpRequestFactory.class,
|
||||
"'requestFactoryType' must be an implementation of ClientHttpRequestFactory");
|
||||
if (requestFactoryType == HttpComponentsClientHttpRequestFactory.class) {
|
||||
return (ClientHttpRequestFactoryBuilder<T>) httpComponents();
|
||||
}
|
||||
if (requestFactoryType == JettyClientHttpRequestFactory.class) {
|
||||
return (ClientHttpRequestFactoryBuilder<T>) jetty();
|
||||
}
|
||||
if (requestFactoryType == ReactorClientHttpRequestFactory.class) {
|
||||
return (ClientHttpRequestFactoryBuilder<T>) reactor();
|
||||
}
|
||||
if (requestFactoryType == JdkClientHttpRequestFactory.class) {
|
||||
return (ClientHttpRequestFactoryBuilder<T>) jdk();
|
||||
}
|
||||
if (requestFactoryType == SimpleClientHttpRequestFactory.class) {
|
||||
return (ClientHttpRequestFactoryBuilder<T>) simple();
|
||||
}
|
||||
return new ReflectiveComponentsClientHttpRequestFactoryBuilder<>(requestFactoryType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactoryBuilder} from the given supplier, using
|
||||
* reflection to ultimately apply the {@link ClientHttpRequestFactorySettings}.
|
||||
* @param <T> the {@link ClientHttpRequestFactory} type
|
||||
* @param requestFactorySupplier the {@link ClientHttpRequestFactory} supplier
|
||||
* @return a new {@link ClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
static <T extends ClientHttpRequestFactory> ClientHttpRequestFactoryBuilder<T> of(
|
||||
Supplier<T> requestFactorySupplier) {
|
||||
return new ReflectiveComponentsClientHttpRequestFactoryBuilder<>(requestFactorySupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the most suitable {@link ClientHttpRequestFactoryBuilder} based on the
|
||||
* classpath. The method favors builders in the following order:
|
||||
* <ol>
|
||||
* <li>{@link #httpComponents()}</li>
|
||||
* <li>{@link #jetty()}</li>
|
||||
* <li>{@link #reactor()}</li>
|
||||
* <li>{@link #jdk()}</li>
|
||||
* <li>{@link #simple()}</li>
|
||||
* </ol>
|
||||
* @return the most suitable {@link ClientHttpRequestFactoryBuilder} for the classpath
|
||||
*/
|
||||
static ClientHttpRequestFactoryBuilder<? extends ClientHttpRequestFactory> detect() {
|
||||
return detect(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the most suitable {@link ClientHttpRequestFactoryBuilder} based on the
|
||||
* classpath. The method favors builders in the following order:
|
||||
* <ol>
|
||||
* <li>{@link #httpComponents()}</li>
|
||||
* <li>{@link #jetty()}</li>
|
||||
* <li>{@link #reactor()}</li>
|
||||
* <li>{@link #jdk()}</li>
|
||||
* <li>{@link #simple()}</li>
|
||||
* </ol>
|
||||
* @param classLoader the class loader to use for detection
|
||||
* @return the most suitable {@link ClientHttpRequestFactoryBuilder} for the classpath
|
||||
* @since 3.5.0
|
||||
*/
|
||||
static ClientHttpRequestFactoryBuilder<? extends ClientHttpRequestFactory> detect(ClassLoader classLoader) {
|
||||
if (HttpComponentsClientHttpRequestFactoryBuilder.Classes.present(classLoader)) {
|
||||
return httpComponents();
|
||||
}
|
||||
if (JettyClientHttpRequestFactoryBuilder.Classes.present(classLoader)) {
|
||||
return jetty();
|
||||
}
|
||||
if (ReactorClientHttpRequestFactoryBuilder.Classes.present(classLoader)) {
|
||||
return reactor();
|
||||
}
|
||||
if (JdkClientHttpRequestFactoryBuilder.Classes.present(classLoader)) {
|
||||
return jdk();
|
||||
}
|
||||
return simple();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
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;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link RuntimeHintsRegistrar} for {@link ClientHttpRequestFactory} implementations.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpRequestFactoryRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
if (ClassUtils.isPresent("org.springframework.http.client.ClientHttpRequestFactory", classLoader)) {
|
||||
registerHints(hints.reflection(), classLoader);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerHints(ReflectionHints hints, ClassLoader classLoader) {
|
||||
hints.registerField(findField(AbstractClientHttpRequestFactoryWrapper.class, "requestFactory"));
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private void registerClientHttpRequestFactoryHints(ReflectionHints hints, ClassLoader classLoader, String className,
|
||||
Runnable action) {
|
||||
hints.registerTypeIfPresent(classLoader, className, (typeHint) -> {
|
||||
typeHint.onReachableType(TypeReference.of(className));
|
||||
action.run();
|
||||
});
|
||||
}
|
||||
|
||||
private void registerReflectionHints(ReflectionHints hints,
|
||||
Class<? extends ClientHttpRequestFactory> requestFactoryType) {
|
||||
registerReflectionHints(hints, requestFactoryType, int.class);
|
||||
}
|
||||
|
||||
private void registerReflectionHints(ReflectionHints hints,
|
||||
Class<? extends ClientHttpRequestFactory> requestFactoryType, Class<?> readTimeoutType) {
|
||||
registerMethod(hints, requestFactoryType, "setConnectTimeout", int.class);
|
||||
registerMethod(hints, requestFactoryType, "setReadTimeout", readTimeoutType);
|
||||
}
|
||||
|
||||
private void registerMethod(ReflectionHints hints, Class<? extends ClientHttpRequestFactory> requestFactoryType,
|
||||
String methodName, Class<?>... parameterTypes) {
|
||||
Method method = ReflectionUtils.findMethod(requestFactoryType, methodName, parameterTypes);
|
||||
if (method != null) {
|
||||
hints.registerMethod(method, ExecutableMode.INVOKE);
|
||||
}
|
||||
}
|
||||
|
||||
private Field findField(Class<?> type, String name) {
|
||||
Field field = ReflectionUtils.findField(type, name);
|
||||
Assert.state(field != null, () -> "Unable to find field '%s' on %s".formatted(type.getName(), name));
|
||||
return field;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.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 redirects the follow redirect strategy to use or null to redirect whenever the
|
||||
* underlying library allows it
|
||||
* @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(HttpRedirects redirects, Duration connectTimeout, Duration readTimeout,
|
||||
SslBundle sslBundle) {
|
||||
|
||||
private static final ClientHttpRequestFactorySettings defaults = new ClientHttpRequestFactorySettings(null, null,
|
||||
null, null);
|
||||
|
||||
public ClientHttpRequestFactorySettings {
|
||||
redirects = (redirects != null) ? redirects : HttpRedirects.FOLLOW_WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(this.redirects, 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.redirects, this.connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
|
||||
* connect and read timeout setting.
|
||||
* @param connectTimeout the new connect timeout setting
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpRequestFactorySettings} instance
|
||||
*/
|
||||
public ClientHttpRequestFactorySettings withTimeouts(Duration connectTimeout, Duration readTimeout) {
|
||||
return new ClientHttpRequestFactorySettings(this.redirects, 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.redirects, this.connectTimeout, this.readTimeout, sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
|
||||
* redirect setting.
|
||||
* @param redirects the new redirects setting
|
||||
* @return a new {@link ClientHttpRequestFactorySettings} instance
|
||||
*/
|
||||
public ClientHttpRequestFactorySettings withRedirects(HttpRedirects redirects) {
|
||||
return new ClientHttpRequestFactorySettings(redirects, this.connectTimeout, this.readTimeout, this.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Helper for empty functional interfaces.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class Empty {
|
||||
|
||||
private static final Consumer<?> EMPTY_CUSTOMIZER = (t) -> {
|
||||
};
|
||||
|
||||
private Empty() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Consumer<T> consumer() {
|
||||
return (Consumer<T>) EMPTY_CUSTOMIZER;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
|
||||
/**
|
||||
* Settings that can be applied when creating a blocking or reactive HTTP client.
|
||||
*
|
||||
* @param redirects the follow redirect strategy to use or null to redirect whenever the
|
||||
* underlying library allows it
|
||||
* @param connectTimeout the connect timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param sslBundle the SSL bundle providing SSL configuration
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public record HttpClientSettings(HttpRedirects redirects, Duration connectTimeout, Duration readTimeout,
|
||||
SslBundle sslBundle) {
|
||||
|
||||
static final HttpClientSettings DEFAULTS = new HttpClientSettings(null, null, null, null);
|
||||
|
||||
public HttpClientSettings {
|
||||
redirects = (redirects != null) ? redirects : HttpRedirects.FOLLOW_WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
|
||||
import org.apache.hc.core5.http.io.SocketConfig;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpRequestFactoryBuilder#httpComponents()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<HttpComponentsClientHttpRequestFactory> {
|
||||
|
||||
private final HttpComponentsHttpClientBuilder httpClientBuilder;
|
||||
|
||||
HttpComponentsClientHttpRequestFactoryBuilder() {
|
||||
this(null, new HttpComponentsHttpClientBuilder());
|
||||
}
|
||||
|
||||
private HttpComponentsClientHttpRequestFactoryBuilder(
|
||||
List<Consumer<HttpComponentsClientHttpRequestFactory>> customizers,
|
||||
HttpComponentsHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withCustomizer(
|
||||
Consumer<HttpComponentsClientHttpRequestFactory> customizer) {
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<HttpComponentsClientHttpRequestFactory>> customizers) {
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizers),
|
||||
this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies
|
||||
* additional customization to the underlying {@link HttpClientBuilder}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withHttpClientCustomizer(
|
||||
Consumer<HttpClientBuilder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link PoolingHttpClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withConnectionManagerCustomizer(connectionManagerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link org.apache.hc.core5.http.io.SocketConfig.Builder}.
|
||||
* @param socketConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withSocketConfigCustomizer(
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer) {
|
||||
Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withSocketConfigCustomizer(socketConfigCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} with a
|
||||
* replacement {@link TlsSocketStrategy} factory.
|
||||
* @param tlsSocketStrategyFactory the new factory used to create a
|
||||
* {@link TlsSocketStrategy} for a given {@link SslBundle}
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withTlsSocketStrategyFactory(
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withTlsSocketStrategyFactory(tlsSocketStrategyFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpComponentsClientHttpRequestFactory createClientHttpRequestFactory(
|
||||
ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withConnectTimeout(null)));
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(factory::setConnectTimeout);
|
||||
return factory;
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.classic.HttpClients";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENTS, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.async.HttpAsyncClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link HttpAsyncClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
|
||||
private final Consumer<HttpAsyncClientBuilder> customizer;
|
||||
|
||||
private final Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer;
|
||||
|
||||
private final Consumer<ConnectionConfig.Builder> connectionConfigCustomizer;
|
||||
|
||||
private final Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer;
|
||||
|
||||
private final Function<SslBundle, TlsStrategy> tlsStrategyFactory;
|
||||
|
||||
public HttpComponentsHttpAsyncClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
|
||||
HttpComponentsSslBundleTlsStrategy::get);
|
||||
}
|
||||
|
||||
private HttpComponentsHttpAsyncClientBuilder(Consumer<HttpAsyncClientBuilder> customizer,
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer,
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
Function<SslBundle, TlsStrategy> tlsStrategyFactory) {
|
||||
this.customizer = customizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.connectionConfigCustomizer = connectionConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsStrategyFactory = tlsStrategyFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpAsyncClientBuilder}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withCustomizer(Consumer<HttpAsyncClientBuilder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer.andThen(customizer),
|
||||
this.connectionManagerCustomizer, this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying {@link PoolingAsyncClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.ConnectionConfig.Builder}.
|
||||
* @param connectionConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withConnectionConfigCustomizer(
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer) {
|
||||
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer.andThen(connectionConfigCustomizer),
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} with a replacement
|
||||
* {@link TlsStrategy} factory.
|
||||
* @param tlsStrategyFactory the new factory used to create a {@link TlsStrategy} for
|
||||
* a given {@link SslBundle}
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withTlsStrategyFactory(
|
||||
Function<SslBundle, TlsStrategy> tlsStrategyFactory) {
|
||||
Assert.notNull(tlsStrategyFactory, "'tlsStrategyFactory' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer), this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpAsyncClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link CloseableHttpAsyncClient} instance
|
||||
*/
|
||||
public CloseableHttpAsyncClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
HttpAsyncClientBuilder builder = HttpAsyncClientBuilder.create()
|
||||
.useSystemProperties()
|
||||
.setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects()))
|
||||
.setConnectionManager(createConnectionManager(settings))
|
||||
.setDefaultRequestConfig(createDefaultRequestConfig());
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private PoolingAsyncClientConnectionManager createConnectionManager(HttpClientSettings settings) {
|
||||
PoolingAsyncClientConnectionManagerBuilder builder = PoolingAsyncClientConnectionManagerBuilder.create()
|
||||
.useSystemProperties();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
builder.setDefaultConnectionConfig(createConnectionConfig(settings));
|
||||
map.from(settings::sslBundle).as(this.tlsStrategyFactory).to(builder::setTlsStrategy);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ConnectionConfig createConnectionConfig(HttpClientSettings settings) {
|
||||
ConnectionConfig.Builder builder = ConnectionConfig.custom();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout)
|
||||
.as(Duration::toMillis)
|
||||
.to((timeout) -> builder.setConnectTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
map.from(settings::readTimeout)
|
||||
.asInt(Duration::toMillis)
|
||||
.to((timeout) -> builder.setSocketTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
this.connectionConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private RequestConfig createDefaultRequestConfig() {
|
||||
RequestConfig.Builder builder = RequestConfig.custom();
|
||||
this.defaultRequestConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
|
||||
import org.apache.hc.core5.http.io.SocketConfig;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class HttpComponentsHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClientBuilder> customizer;
|
||||
|
||||
private final Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer;
|
||||
|
||||
private final Consumer<SocketConfig.Builder> socketConfigCustomizer;
|
||||
|
||||
private final Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer;
|
||||
|
||||
private final Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory;
|
||||
|
||||
public HttpComponentsHttpClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
|
||||
HttpComponentsSslBundleTlsStrategy::get);
|
||||
}
|
||||
|
||||
private HttpComponentsHttpClientBuilder(Consumer<HttpClientBuilder> customizer,
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer,
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
this.customizer = customizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.socketConfigCustomizer = socketConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsSocketStrategyFactory = tlsSocketStrategyFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientBuilder}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withCustomizer(Consumer<HttpClientBuilder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer.andThen(customizer),
|
||||
this.connectionManagerCustomizer, this.socketConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying {@link PoolingHttpClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.core5.http.io.SocketConfig.Builder}.
|
||||
* @param socketConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withSocketConfigCustomizer(
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer) {
|
||||
Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer.andThen(socketConfigCustomizer), this.defaultRequestConfigCustomizer,
|
||||
this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} with a replacement
|
||||
* {@link TlsSocketStrategy} factory.
|
||||
* @param tlsSocketStrategyFactory the new factory used to create a
|
||||
* {@link TlsSocketStrategy}. The function will be provided with a {@link SslBundle}
|
||||
* or {@code null} if no bundle is selected. Only non {@code null} results will be
|
||||
* applied.
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withTlsSocketStrategyFactory(
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.defaultRequestConfigCustomizer, tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer),
|
||||
this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public CloseableHttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
Assert.isTrue(settings.connectTimeout() == null, "'settings' must not have a 'connectTimeout'");
|
||||
HttpClientBuilder builder = HttpClientBuilder.create()
|
||||
.useSystemProperties()
|
||||
.setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects()))
|
||||
.setConnectionManager(createConnectionManager(settings))
|
||||
.setDefaultRequestConfig(createDefaultRequestConfig());
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private PoolingHttpClientConnectionManager createConnectionManager(HttpClientSettings settings) {
|
||||
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.useSystemProperties();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
builder.setDefaultSocketConfig(createSocketConfig(settings));
|
||||
map.from(settings::sslBundle).as(this.tlsSocketStrategyFactory).whenNonNull().to(builder::setTlsSocketStrategy);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private SocketConfig createSocketConfig(HttpClientSettings settings) {
|
||||
SocketConfig.Builder builder = SocketConfig.custom();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout)
|
||||
.asInt(Duration::toMillis)
|
||||
.to((timeout) -> builder.setSoTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
this.socketConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private RequestConfig createDefaultRequestConfig() {
|
||||
RequestConfig.Builder builder = RequestConfig.custom();
|
||||
this.defaultRequestConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||
import org.apache.hc.client5.http.protocol.RedirectStrategy;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
|
||||
/**
|
||||
* Adapts {@link HttpRedirects} to an
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link RedirectStrategy}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class HttpComponentsRedirectStrategy {
|
||||
|
||||
private HttpComponentsRedirectStrategy() {
|
||||
}
|
||||
|
||||
static RedirectStrategy get(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> DefaultRedirectStrategy.INSTANCE;
|
||||
case DONT_FOLLOW -> NoFollowRedirectStrategy.INSTANCE;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link RedirectStrategy} that never follows redirects.
|
||||
*/
|
||||
private static final class NoFollowRedirectStrategy implements RedirectStrategy {
|
||||
|
||||
private static final RedirectStrategy INSTANCE = new NoFollowRedirectStrategy();
|
||||
|
||||
private NoFollowRedirectStrategy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
||||
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
|
||||
/**
|
||||
* Adapts {@link SslBundle} to an
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link DefaultClientTlsStrategy}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class HttpComponentsSslBundleTlsStrategy {
|
||||
|
||||
private HttpComponentsSslBundleTlsStrategy() {
|
||||
}
|
||||
|
||||
static DefaultClientTlsStrategy get(SslBundle sslBundle) {
|
||||
if (sslBundle == null) {
|
||||
return null;
|
||||
}
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLContext sslContext = sslBundle.createSslContext();
|
||||
return new DefaultClientTlsStrategy(sslContext, options.getEnabledProtocols(), options.getCiphers(), null,
|
||||
new DefaultHostnameVerifier());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
/**
|
||||
* Redirect strategies support by HTTP clients.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public enum HttpRedirects {
|
||||
|
||||
/**
|
||||
* Follow redirects (if the underlying library has support).
|
||||
*/
|
||||
FOLLOW_WHEN_POSSIBLE,
|
||||
|
||||
/**
|
||||
* Follow redirects (fail if the underlying library has no support).
|
||||
*/
|
||||
FOLLOW,
|
||||
|
||||
/**
|
||||
* Don't follow redirects (fail if the underlying library has no support).
|
||||
*/
|
||||
DONT_FOLLOW
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpRequestFactoryBuilder#jdk()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public final class JdkClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<JdkClientHttpRequestFactory> {
|
||||
|
||||
private final JdkHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JdkClientHttpRequestFactoryBuilder() {
|
||||
this(null, new JdkHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JdkClientHttpRequestFactoryBuilder(List<Consumer<JdkClientHttpRequestFactory>> customizers,
|
||||
JdkHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpRequestFactoryBuilder withCustomizer(Consumer<JdkClientHttpRequestFactory> customizer) {
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<JdkClientHttpRequestFactory>> customizers) {
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link JdkClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JdkClientHttpRequestFactoryBuilder withHttpClientCustomizer(
|
||||
Consumer<HttpClient.Builder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JdkClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withReadTimeout(null)));
|
||||
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout).to(requestFactory::setReadTimeout);
|
||||
return requestFactory;
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "java.net.http.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLParameters;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a JDK {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JdkHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClient.Builder> customizer;
|
||||
|
||||
public JdkHttpClientBuilder() {
|
||||
this(Empty.consumer());
|
||||
}
|
||||
|
||||
private JdkHttpClientBuilder(Consumer<HttpClient.Builder> customizer) {
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkHttpClientBuilder} that applies additional customization to
|
||||
* the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link JdkHttpClientBuilder} instance
|
||||
*/
|
||||
public JdkHttpClientBuilder withCustomizer(Consumer<HttpClient.Builder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new JdkHttpClientBuilder(this.customizer.andThen(customizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public HttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
Assert.isTrue(settings.readTimeout() == null, "'settings' must not have a 'readTimeout'");
|
||||
HttpClient.Builder builder = HttpClient.newBuilder();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::redirects).as(this::asHttpClientRedirect).to(builder::followRedirects);
|
||||
map.from(settings::connectTimeout).to(builder::connectTimeout);
|
||||
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext);
|
||||
map.from(settings::sslBundle).as(this::asSslParameters).to(builder::sslParameters);
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private SSLParameters asSslParameters(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLParameters parameters = new SSLParameters();
|
||||
parameters.setCipherSuites(options.getCiphers());
|
||||
parameters.setProtocols(options.getEnabledProtocols());
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private Redirect asHttpClientRedirect(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> Redirect.NORMAL;
|
||||
case DONT_FOLLOW -> Redirect.NEVER;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpRequestFactoryBuilder#jetty()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public final class JettyClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> {
|
||||
|
||||
private final JettyHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JettyClientHttpRequestFactoryBuilder() {
|
||||
this(null, new JettyHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JettyClientHttpRequestFactoryBuilder(List<Consumer<JettyClientHttpRequestFactory>> customizers,
|
||||
JettyHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpRequestFactoryBuilder withCustomizer(Consumer<JettyClientHttpRequestFactory> customizer) {
|
||||
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<JettyClientHttpRequestFactory>> customizers) {
|
||||
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpRequestFactoryBuilder withHttpClientCustomizer(Consumer<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientTransport}.
|
||||
* @param httpClientTransportCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpRequestFactoryBuilder withHttpClientTransportCustomizer(
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer) {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientTransportCustomizer(httpClientTransportCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link ClientConnector}.
|
||||
* @param clientConnectorCustomizerCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpRequestFactoryBuilder withClientConnectorCustomizerCustomizer(
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JettyClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withTimeouts(null, null)));
|
||||
JettyClientHttpRequestFactory requestFactory = new JettyClientHttpRequestFactory(httpClient);
|
||||
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;
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "org.eclipse.jetty.client.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.Request;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
|
||||
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.boot.ssl.SslOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a Jetty {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JettyHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClient> customizer;
|
||||
|
||||
private final Consumer<HttpClientTransport> httpClientTransportCustomizer;
|
||||
|
||||
private final Consumer<ClientConnector> clientConnectorCustomizerCustomizer;
|
||||
|
||||
public JettyHttpClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer());
|
||||
}
|
||||
|
||||
private JettyHttpClientBuilder(Consumer<HttpClient> customizer,
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer,
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
this.customizer = customizer;
|
||||
this.httpClientTransportCustomizer = httpClientTransportCustomizer;
|
||||
this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyHttpClientBuilder withCustomizer(Consumer<HttpClient> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer.andThen(customizer), this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientTransport}.
|
||||
* @param httpClientTransportCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyHttpClientBuilder withHttpClientTransportCustomizer(
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer) {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer,
|
||||
this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer),
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link ClientConnector}.
|
||||
* @param clientConnectorCustomizerCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyHttpClientBuilder withClientConnectorCustomizerCustomizer(
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public HttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
HttpClientTransport transport = createTransport(settings);
|
||||
this.httpClientTransportCustomizer.accept(transport);
|
||||
HttpClient httpClient = createHttpClient(settings.readTimeout(), transport);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout);
|
||||
map.from(settings::redirects).as(this::followRedirects).to(httpClient::setFollowRedirects);
|
||||
this.customizer.accept(httpClient);
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient(Duration readTimeout, HttpClientTransport transport) {
|
||||
return (readTimeout != null) ? new HttpClientWithReadTimeout(transport, readTimeout)
|
||||
: new HttpClient(transport);
|
||||
}
|
||||
|
||||
private HttpClientTransport createTransport(HttpClientSettings settings) {
|
||||
ClientConnector connector = createClientConnector(settings.sslBundle());
|
||||
return (connector.getSslContextFactory() != null) ? new HttpClientTransportDynamic(connector)
|
||||
: new HttpClientTransportOverHTTP(connector);
|
||||
}
|
||||
|
||||
private ClientConnector createClientConnector(SslBundle sslBundle) {
|
||||
ClientConnector connector = new ClientConnector();
|
||||
if (sslBundle != null) {
|
||||
connector.setSslContextFactory(createSslContextFactory(sslBundle));
|
||||
}
|
||||
this.clientConnectorCustomizerCustomizer.accept(connector);
|
||||
return connector;
|
||||
}
|
||||
|
||||
private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLContext sslContext = sslBundle.createSslContext();
|
||||
SslContextFactory.Client factory = new SslContextFactory.Client();
|
||||
factory.setSslContext(sslContext);
|
||||
if (options.getCiphers() != null) {
|
||||
factory.setIncludeCipherSuites(options.getCiphers());
|
||||
factory.setExcludeCipherSuites();
|
||||
}
|
||||
if (options.getEnabledProtocols() != null) {
|
||||
factory.setIncludeProtocols(options.getEnabledProtocols());
|
||||
factory.setExcludeProtocols();
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
private boolean followRedirects(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> true;
|
||||
case DONT_FOLLOW -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HttpClient} subclass that sets the read timeout.
|
||||
*/
|
||||
static class HttpClientWithReadTimeout extends HttpClient {
|
||||
|
||||
private final Duration readTimeout;
|
||||
|
||||
HttpClientWithReadTimeout(HttpClientTransport transport, Duration readTimeout) {
|
||||
super(transport);
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.jetty.client.Request newRequest(java.net.URI uri) {
|
||||
Request request = super.newRequest(uri);
|
||||
request.timeout(this.readTimeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpRequestFactoryBuilder#reactor()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public final class ReactorClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<ReactorClientHttpRequestFactory> {
|
||||
|
||||
private final ReactorHttpClientBuilder httpClientBuilder;
|
||||
|
||||
ReactorClientHttpRequestFactoryBuilder() {
|
||||
this(null, new ReactorHttpClientBuilder());
|
||||
}
|
||||
|
||||
private ReactorClientHttpRequestFactoryBuilder(List<Consumer<ReactorClientHttpRequestFactory>> customizers,
|
||||
ReactorHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpRequestFactoryBuilder withCustomizer(Consumer<ReactorClientHttpRequestFactory> customizer) {
|
||||
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<ReactorClientHttpRequestFactory>> customizers) {
|
||||
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that uses the given
|
||||
* {@link ReactorResourceFactory} to create the underlying {@link HttpClient}.
|
||||
* @param reactorResourceFactory the {@link ReactorResourceFactory} to use
|
||||
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public ReactorClientHttpRequestFactoryBuilder withReactorResourceFactory(
|
||||
ReactorResourceFactory reactorResourceFactory) {
|
||||
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
|
||||
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withReactorResourceFactory(reactorResourceFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that uses the given
|
||||
* factory to create the underlying {@link HttpClient}.
|
||||
* @param factory the factory to use
|
||||
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public ReactorClientHttpRequestFactoryBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
|
||||
Assert.notNull(factory, "'factory' must not be null");
|
||||
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientFactory(factory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public ReactorClientHttpRequestFactoryBuilder withHttpClientCustomizer(
|
||||
UnaryOperator<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactorClientHttpRequestFactory createClientHttpRequestFactory(
|
||||
ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withTimeouts(null, null)));
|
||||
ReactorClientHttpRequestFactory requestFactory = new ReactorClientHttpRequestFactory(httpClient);
|
||||
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;
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "reactor.netty.http.client.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
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.ReactorResourceFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.function.ThrowingConsumer;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a Rector Netty {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class ReactorHttpClientBuilder {
|
||||
|
||||
private final Supplier<HttpClient> factory;
|
||||
|
||||
private final UnaryOperator<HttpClient> customizer;
|
||||
|
||||
public ReactorHttpClientBuilder() {
|
||||
this(HttpClient::create, UnaryOperator.identity());
|
||||
}
|
||||
|
||||
private ReactorHttpClientBuilder(Supplier<HttpClient> httpClientFactory, UnaryOperator<HttpClient> customizer) {
|
||||
this.factory = httpClientFactory;
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that uses the given
|
||||
* {@link ReactorResourceFactory} to create the {@link HttpClient}.
|
||||
* @param reactorResourceFactory the {@link ReactorResourceFactory} to use
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
*/
|
||||
public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) {
|
||||
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
|
||||
return new ReactorHttpClientBuilder(() -> HttpClient.create(reactorResourceFactory.getConnectionProvider()),
|
||||
(httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that uses the given factory to create
|
||||
* the {@link HttpClient}.
|
||||
* @param factory the factory to use
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
*/
|
||||
public ReactorHttpClientBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
|
||||
Assert.notNull(factory, "'factory' must not be null");
|
||||
return new ReactorHttpClientBuilder(factory, this.customizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that applies additional customization
|
||||
* to the underlying {@link HttpClient}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
*/
|
||||
public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator<HttpClient> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new ReactorHttpClientBuilder(this.factory,
|
||||
(httpClient) -> customizer.apply(this.customizer.apply(httpClient)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public HttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
HttpClient httpClient = applyDefaults(this.factory.get());
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
httpClient = map.from(settings::connectTimeout).to(httpClient, this::setConnectTimeout);
|
||||
httpClient = map.from(settings::readTimeout).to(httpClient, HttpClient::responseTimeout);
|
||||
httpClient = map.from(settings::redirects).as(this::followRedirects).to(httpClient, HttpClient::followRedirect);
|
||||
httpClient = map.from(settings::sslBundle).to(httpClient, this::secure);
|
||||
return this.customizer.apply(httpClient);
|
||||
}
|
||||
|
||||
HttpClient applyDefaults(HttpClient httpClient) {
|
||||
// Aligns with Spring Framework defaults
|
||||
return httpClient.compress(true);
|
||||
}
|
||||
|
||||
private HttpClient setConnectTimeout(HttpClient httpClient, Duration timeout) {
|
||||
return httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) timeout.toMillis());
|
||||
}
|
||||
|
||||
private boolean followRedirects(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> true;
|
||||
case DONT_FOLLOW -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private HttpClient secure(HttpClient httpClient, SslBundle sslBundle) {
|
||||
return httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, sslBundle))));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Internal builder for {@link ClientHttpRequestFactoryBuilder#of(Class)} and
|
||||
* {@link ClientHttpRequestFactoryBuilder#of(Supplier)}.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpRequestFactory} type
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
final class ReflectiveComponentsClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory>
|
||||
implements ClientHttpRequestFactoryBuilder<T> {
|
||||
|
||||
private final Supplier<T> requestFactorySupplier;
|
||||
|
||||
ReflectiveComponentsClientHttpRequestFactoryBuilder(Supplier<T> requestFactorySupplier) {
|
||||
Assert.notNull(requestFactorySupplier, "'requestFactorySupplier' must not be null");
|
||||
this.requestFactorySupplier = requestFactorySupplier;
|
||||
}
|
||||
|
||||
ReflectiveComponentsClientHttpRequestFactoryBuilder(Class<T> requestFactoryType) {
|
||||
Assert.notNull(requestFactoryType, "'requestFactoryType' must not be null");
|
||||
this.requestFactorySupplier = () -> createRequestFactory(requestFactoryType);
|
||||
}
|
||||
|
||||
private static <T extends ClientHttpRequestFactory> T createRequestFactory(Class<T> requestFactory) {
|
||||
try {
|
||||
Constructor<T> constructor = requestFactory.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T build(ClientHttpRequestFactorySettings settings) {
|
||||
T requestFactory = this.requestFactorySupplier.get();
|
||||
if (settings != null) {
|
||||
configure(requestFactory, settings);
|
||||
}
|
||||
return requestFactory;
|
||||
}
|
||||
|
||||
private void configure(ClientHttpRequestFactory requestFactory, ClientHttpRequestFactorySettings settings) {
|
||||
Assert.state(settings.sslBundle() == null, "Unable to set SSL bundle using reflection");
|
||||
Assert.state(settings.redirects() == HttpRedirects.FOLLOW_WHEN_POSSIBLE,
|
||||
"Unable to set redirect follow 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpRequestFactoryBuilder#simple()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public final class SimpleClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<SimpleClientHttpRequestFactory> {
|
||||
|
||||
SimpleClientHttpRequestFactoryBuilder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
private SimpleClientHttpRequestFactoryBuilder(List<Consumer<SimpleClientHttpRequestFactory>> customizers) {
|
||||
super(customizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleClientHttpRequestFactoryBuilder withCustomizer(Consumer<SimpleClientHttpRequestFactory> customizer) {
|
||||
return new SimpleClientHttpRequestFactoryBuilder(mergedCustomizers(customizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<SimpleClientHttpRequestFactory>> customizers) {
|
||||
return new SimpleClientHttpRequestFactoryBuilder(mergedCustomizers(customizers));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SimpleClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
SslBundle sslBundle = settings.sslBundle();
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpsRequestFactory(settings);
|
||||
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}
|
||||
* and {@link Redirects}.
|
||||
*/
|
||||
private static class SimpleClientHttpsRequestFactory extends SimpleClientHttpRequestFactory {
|
||||
|
||||
private final ClientHttpRequestFactorySettings settings;
|
||||
|
||||
SimpleClientHttpsRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
|
||||
super.prepareConnection(connection, httpMethod);
|
||||
if (this.settings.sslBundle() != null && connection instanceof HttpsURLConnection secureConnection) {
|
||||
SSLSocketFactory socketFactory = this.settings.sslBundle().createSslContext().getSocketFactory();
|
||||
secureConnection.setSSLSocketFactory(socketFactory);
|
||||
}
|
||||
if (this.settings.redirects() == HttpRedirects.DONT_FOLLOW) {
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for web clients.
|
||||
* Client-side HTTP support classes.
|
||||
*/
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
package org.springframework.boot.http.client;
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
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.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Internal base class used for {@link ClientHttpConnectorBuilder} implementations.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
abstract class AbstractClientHttpConnectorBuilder<T extends ClientHttpConnector>
|
||||
implements ClientHttpConnectorBuilder<T> {
|
||||
|
||||
private final List<Consumer<T>> customizers;
|
||||
|
||||
protected AbstractClientHttpConnectorBuilder(List<Consumer<T>> customizers) {
|
||||
this.customizers = (customizers != null) ? customizers : Collections.emptyList();
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> getCustomizers() {
|
||||
return this.customizers;
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> mergedCustomizers(Consumer<T> customizer) {
|
||||
Assert.notNull(this.customizers, "'customizer' must not be null");
|
||||
return merge(this.customizers, List.of(customizer));
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> mergedCustomizers(Collection<Consumer<T>> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
|
||||
return merge(this.customizers, customizers);
|
||||
}
|
||||
|
||||
private <E> List<E> merge(Collection<E> list, Collection<? extends E> additional) {
|
||||
List<E> merged = new ArrayList<>(list);
|
||||
merged.addAll(additional);
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final T build(ClientHttpConnectorSettings settings) {
|
||||
T connector = createClientHttpConnector((settings != null) ? settings : ClientHttpConnectorSettings.defaults());
|
||||
LambdaSafe.callbacks(Consumer.class, this.customizers, connector)
|
||||
.invoke((consumer) -> consumer.accept(connector));
|
||||
return connector;
|
||||
}
|
||||
|
||||
protected abstract T createClientHttpConnector(ClientHttpConnectorSettings settings);
|
||||
|
||||
protected final HttpClientSettings asHttpClientSettings(ClientHttpConnectorSettings settings) {
|
||||
return (settings != null) ? new HttpClientSettings(settings.redirects(), settings.connectTimeout(),
|
||||
settings.readTimeout(), settings.sslBundle()) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Interface used to build a fully configured {@link ClientHttpConnector}. Builders for
|
||||
* {@link #reactor() Reactor}, {@link #jetty() Jetty}, {@link #httpComponents() Apache
|
||||
* HTTP Components} and {@link #jdk() JDK} can be obtained using the factory methods on
|
||||
* this interface. The {@link #of(Class)} method may be used to instantiate based on the
|
||||
* connector type.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClientHttpConnectorBuilder<T extends ClientHttpConnector> {
|
||||
|
||||
/**
|
||||
* Build a default configured {@link ClientHttpConnectorBuilder}.
|
||||
* @return a default configured {@link ClientHttpConnectorBuilder}.
|
||||
*/
|
||||
default T build() {
|
||||
return build(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully configured {@link ClientHttpConnector}, applying the given
|
||||
* {@code settings} if they are provided.
|
||||
* @param settings the settings to apply or {@code null}
|
||||
* @return a fully configured {@link ClientHttpConnector}.
|
||||
*/
|
||||
T build(ClientHttpConnectorSettings settings);
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorBuilder} that applies the given customizer
|
||||
* to the {@link ClientHttpConnector} after it has been built.
|
||||
* @param customizer the customizers to apply
|
||||
* @return a new {@link ClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
default ClientHttpConnectorBuilder<T> withCustomizer(Consumer<T> customizer) {
|
||||
return withCustomizers(List.of(customizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorBuilder} that applies the given customizers
|
||||
* to the {@link ClientHttpConnector} after it has been built.
|
||||
* @param customizers the customizers to apply
|
||||
* @return a new {@link ClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default ClientHttpConnectorBuilder<T> withCustomizers(Collection<Consumer<T>> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
|
||||
return (settings) -> {
|
||||
T factory = build(settings);
|
||||
LambdaSafe.callbacks(Consumer.class, customizers, factory).invoke((consumer) -> consumer.accept(factory));
|
||||
return factory;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link HttpComponentsClientHttpConnectorBuilder} that can be used to build
|
||||
* a {@link HttpComponentsClientHttpConnector}.
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder}
|
||||
*/
|
||||
static HttpComponentsClientHttpConnectorBuilder httpComponents() {
|
||||
return new HttpComponentsClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link JettyClientHttpConnectorBuilder} that can be used to build a
|
||||
* {@link JettyClientHttpConnector}.
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder}
|
||||
*/
|
||||
static JettyClientHttpConnectorBuilder jetty() {
|
||||
return new JettyClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ReactorClientHttpConnectorBuilder} that can be used to build a
|
||||
* {@link ReactorClientHttpConnector}.
|
||||
* @return a new {@link ReactorClientHttpConnectorBuilder}
|
||||
*/
|
||||
static ReactorClientHttpConnectorBuilder reactor() {
|
||||
return new ReactorClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link JdkClientHttpConnectorBuilder} that can be used to build a
|
||||
* {@link JdkClientHttpConnector} .
|
||||
* @return a new {@link JdkClientHttpConnectorBuilder}
|
||||
*/
|
||||
static JdkClientHttpConnectorBuilder jdk() {
|
||||
return new JdkClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorBuilder} for the given
|
||||
* {@code requestFactoryType}. The following implementations are supported:
|
||||
* <ul>
|
||||
* <li>{@link ReactorClientHttpConnector}</li>
|
||||
* <li>{@link JettyClientHttpConnector}</li>
|
||||
* <li>{@link HttpComponentsClientHttpConnector}</li>
|
||||
* <li>{@link JdkClientHttpConnector}</li>
|
||||
* </ul>
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @param clientHttpConnectorType the {@link ClientHttpConnector} type
|
||||
* @return a new {@link ClientHttpConnectorBuilder}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ClientHttpConnector> ClientHttpConnectorBuilder<T> of(Class<T> clientHttpConnectorType) {
|
||||
Assert.notNull(clientHttpConnectorType, "'requestFactoryType' must not be null");
|
||||
Assert.isTrue(clientHttpConnectorType != ClientHttpConnector.class,
|
||||
"'clientHttpConnectorType' must be an implementation of ClientHttpConnector");
|
||||
if (clientHttpConnectorType == ReactorClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) reactor();
|
||||
}
|
||||
if (clientHttpConnectorType == JettyClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) jetty();
|
||||
}
|
||||
if (clientHttpConnectorType == HttpComponentsClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) httpComponents();
|
||||
}
|
||||
if (clientHttpConnectorType == JdkClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) jdk();
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"'clientHttpConnectorType' %s is not supported".formatted(clientHttpConnectorType.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the most suitable {@link ClientHttpConnectorBuilder} based on the classpath.
|
||||
* The method favors builders in the following order:
|
||||
* <ol>
|
||||
* <li>{@link #reactor()}</li>
|
||||
* <li>{@link #jetty()}</li>
|
||||
* <li>{@link #httpComponents()}</li>
|
||||
* <li>{@link #jdk()}</li>
|
||||
* </ol>
|
||||
* @return the most suitable {@link ClientHttpConnectorBuilder} for the classpath
|
||||
*/
|
||||
static ClientHttpConnectorBuilder<? extends ClientHttpConnector> detect() {
|
||||
return detect(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the most suitable {@link ClientHttpConnectorBuilder} based on the classpath.
|
||||
* The method favors builders in the following order:
|
||||
* <ol>
|
||||
* <li>{@link #reactor()}</li>
|
||||
* <li>{@link #jetty()}</li>
|
||||
* <li>{@link #httpComponents()}</li>
|
||||
* <li>{@link #jdk()}</li>
|
||||
* </ol>
|
||||
* @param classLoader the class loader to use for detection
|
||||
* @return the most suitable {@link ClientHttpConnectorBuilder} for the classpath
|
||||
*/
|
||||
static ClientHttpConnectorBuilder<? extends ClientHttpConnector> detect(ClassLoader classLoader) {
|
||||
if (ReactorClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return reactor();
|
||||
}
|
||||
if (JettyClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return jetty();
|
||||
}
|
||||
if (HttpComponentsClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return httpComponents();
|
||||
}
|
||||
if (JdkClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return jdk();
|
||||
}
|
||||
throw new IllegalStateException("Unable to detect any ClientHttpConnectorBuilder");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
/**
|
||||
* Settings that can be applied when creating a {@link ClientHttpConnector}.
|
||||
*
|
||||
* @param redirects the follow redirect strategy to use or null to redirect whenever the
|
||||
* underlying library allows it
|
||||
* @param connectTimeout the connect timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param sslBundle the SSL bundle providing SSL configuration
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see ClientHttpConnectorBuilder
|
||||
*/
|
||||
public record ClientHttpConnectorSettings(HttpRedirects redirects, Duration connectTimeout, Duration readTimeout,
|
||||
SslBundle sslBundle) {
|
||||
|
||||
private static final ClientHttpConnectorSettings defaults = new ClientHttpConnectorSettings(null, null, null, null);
|
||||
|
||||
public ClientHttpConnectorSettings {
|
||||
redirects = (redirects != null) ? redirects : HttpRedirects.FOLLOW_WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated connect
|
||||
* timeout setting.
|
||||
* @param connectTimeout the new connect timeout setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withConnectTimeout(Duration connectTimeout) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, connectTimeout, this.readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated read
|
||||
* timeout setting.
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withReadTimeout(Duration readTimeout) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated connect
|
||||
* and read timeout setting.
|
||||
* @param connectTimeout the new connect timeout setting
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withTimeouts(Duration connectTimeout, Duration readTimeout) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated SSL
|
||||
* bundle setting.
|
||||
* @param sslBundle the new SSL bundle setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withSslBundle(SslBundle sslBundle) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated redirect
|
||||
* setting.
|
||||
* @param redirects the new redirects setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withRedirects(HttpRedirects redirects) {
|
||||
return new ClientHttpConnectorSettings(redirects, this.connectTimeout, this.readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} using defaults for all settings
|
||||
* other than the provided SSL bundle.
|
||||
* @param sslBundle the SSL bundle setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public static ClientHttpConnectorSettings ofSslBundle(SslBundle sslBundle) {
|
||||
return defaults().withSslBundle(sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use defaults for the {@link ClientHttpConnector} which can differ depending on the
|
||||
* implementation.
|
||||
* @return default settings
|
||||
*/
|
||||
public static ClientHttpConnectorSettings defaults() {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
|
||||
|
||||
import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#httpComponents()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class HttpComponentsClientHttpConnectorBuilder
|
||||
extends AbstractClientHttpConnectorBuilder<HttpComponentsClientHttpConnector> {
|
||||
|
||||
private final HttpComponentsHttpAsyncClientBuilder httpClientBuilder;
|
||||
|
||||
HttpComponentsClientHttpConnectorBuilder() {
|
||||
this(null, new HttpComponentsHttpAsyncClientBuilder());
|
||||
}
|
||||
|
||||
private HttpComponentsClientHttpConnectorBuilder(List<Consumer<HttpComponentsClientHttpConnector>> customizers,
|
||||
HttpComponentsHttpAsyncClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying {@link HttpAsyncClientBuilder}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withHttpClientCustomizer(
|
||||
Consumer<HttpAsyncClientBuilder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'customizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link PoolingAsyncClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withConnectionManagerCustomizer(connectionManagerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.ConnectionConfig.Builder}.
|
||||
* @param connectionConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withConnectionConfigCustomizer(
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer) {
|
||||
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withConnectionConfigCustomizer(connectionConfigCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} with a replacement
|
||||
* {@link TlsStrategy} factory.
|
||||
* @param tlsStrategyFactory the new factory used to create a {@link TlsStrategy} for
|
||||
* a given {@link SslBundle}
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withTlsSocketStrategyFactory(
|
||||
Function<SslBundle, TlsStrategy> tlsStrategyFactory) {
|
||||
Assert.notNull(tlsStrategyFactory, "'tlsStrategyFactory' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withTlsStrategyFactory(tlsStrategyFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpComponentsClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
CloseableHttpAsyncClient client = this.httpClientBuilder.build(asHttpClientSettings(settings));
|
||||
return new HttpComponentsClientHttpConnector(client);
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.async.HttpAsyncClients";
|
||||
|
||||
static final String REACTIVE_RESPONSE_CONSUMER = "org.apache.hc.core5.reactive.ReactiveResponseConsumer";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENTS, classLoader)
|
||||
&& ClassUtils.isPresent(REACTIVE_RESPONSE_CONSUMER, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.JdkHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#jdk()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JdkClientHttpConnectorBuilder extends AbstractClientHttpConnectorBuilder<JdkClientHttpConnector> {
|
||||
|
||||
private final JdkHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JdkClientHttpConnectorBuilder() {
|
||||
this(null, new JdkHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JdkClientHttpConnectorBuilder(List<Consumer<JdkClientHttpConnector>> customizers,
|
||||
JdkHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpConnectorBuilder withCustomizer(Consumer<JdkClientHttpConnector> customizer) {
|
||||
return new JdkClientHttpConnectorBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpConnectorBuilder withCustomizers(Collection<Consumer<JdkClientHttpConnector>> customizers) {
|
||||
return new JdkClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link JdkClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JdkClientHttpConnectorBuilder withHttpClientCustomizer(Consumer<HttpClient.Builder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JdkClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JdkClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withReadTimeout(null)));
|
||||
JdkClientHttpConnector connector = new JdkClientHttpConnector(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout).to(connector::setReadTimeout);
|
||||
return connector;
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "java.net.http.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
|
||||
import org.springframework.boot.http.client.JettyHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#jetty()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JettyClientHttpConnectorBuilder
|
||||
extends AbstractClientHttpConnectorBuilder<JettyClientHttpConnector> {
|
||||
|
||||
private final JettyHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JettyClientHttpConnectorBuilder() {
|
||||
this(null, new JettyHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JettyClientHttpConnectorBuilder(List<Consumer<JettyClientHttpConnector>> customizers,
|
||||
JettyHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpConnectorBuilder withCustomizer(Consumer<JettyClientHttpConnector> customizer) {
|
||||
return new JettyClientHttpConnectorBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpConnectorBuilder withCustomizers(Collection<Consumer<JettyClientHttpConnector>> customizers) {
|
||||
return new JettyClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withHttpClientCustomizer(Consumer<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientTransport}.
|
||||
* @param httpClientTransportCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withHttpClientTransportCustomizer(
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer) {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientTransportCustomizer(httpClientTransportCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link ClientConnector}.
|
||||
* @param clientConnectorCustomizerCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withClientConnectorCustomizerCustomizer(
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JettyClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings));
|
||||
return new JettyClientHttpConnector(httpClient);
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "org.eclipse.jetty.client.HttpClient";
|
||||
|
||||
static final String REACTIVE_REQUEST = "org.eclipse.jetty.reactive.client.ReactiveRequest";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader)
|
||||
&& ClassUtils.isPresent(REACTIVE_REQUEST, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#reactor()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class ReactorClientHttpConnectorBuilder
|
||||
extends AbstractClientHttpConnectorBuilder<ReactorClientHttpConnector> {
|
||||
|
||||
private final ReactorHttpClientBuilder httpClientBuilder;
|
||||
|
||||
ReactorClientHttpConnectorBuilder() {
|
||||
this(null, new ReactorHttpClientBuilder());
|
||||
}
|
||||
|
||||
private ReactorClientHttpConnectorBuilder(List<Consumer<ReactorClientHttpConnector>> customizers,
|
||||
ReactorHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpConnectorBuilder withCustomizer(Consumer<ReactorClientHttpConnector> customizer) {
|
||||
return new ReactorClientHttpConnectorBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpConnectorBuilder withCustomizers(
|
||||
Collection<Consumer<ReactorClientHttpConnector>> customizers) {
|
||||
return new ReactorClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpConnectorBuilder} that uses the given
|
||||
* {@link ReactorResourceFactory} to create the underlying {@link HttpClient}.
|
||||
* @param reactorResourceFactory the {@link ReactorResourceFactory} to use
|
||||
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public ReactorClientHttpConnectorBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) {
|
||||
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
|
||||
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withReactorResourceFactory(reactorResourceFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpConnectorBuilder} that uses the given factory
|
||||
* to create the underlying {@link HttpClient}.
|
||||
* @param factory the factory to use
|
||||
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public ReactorClientHttpConnectorBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
|
||||
Assert.notNull(factory, "'factory' must not be null");
|
||||
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientFactory(factory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public ReactorClientHttpConnectorBuilder withHttpClientCustomizer(UnaryOperator<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactorClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings));
|
||||
return new ReactorClientHttpConnector(httpClient);
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "reactor.netty.http.client.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
/**
|
||||
* Base {@link ConfigurationProperties @ConfigurationProperties} for configuring a
|
||||
* {@link ClientHttpConnector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see ClientHttpConnectorSettings
|
||||
*/
|
||||
public abstract class AbstractClientHttpConnectorProperties {
|
||||
|
||||
/**
|
||||
* Handling for HTTP redirects.
|
||||
*/
|
||||
private HttpRedirects redirects;
|
||||
|
||||
/**
|
||||
* Default connect timeout for a client HTTP request.
|
||||
*/
|
||||
private Duration connectTimeout;
|
||||
|
||||
/**
|
||||
* Default read timeout for a client HTTP request.
|
||||
*/
|
||||
private Duration readTimeout;
|
||||
|
||||
/**
|
||||
* Default SSL configuration for a client HTTP request.
|
||||
*/
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
/**
|
||||
* Default connector used for a client HTTP request.
|
||||
*/
|
||||
private Connector connector;
|
||||
|
||||
public HttpRedirects getRedirects() {
|
||||
return this.redirects;
|
||||
}
|
||||
|
||||
public void setRedirects(HttpRedirects redirects) {
|
||||
this.redirects = redirects;
|
||||
}
|
||||
|
||||
public Duration getConnectTimeout() {
|
||||
return this.connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Duration connectTimeout) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
public Duration getReadTimeout() {
|
||||
return this.readTimeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(Duration readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public Connector getConnector() {
|
||||
return this.connector;
|
||||
}
|
||||
|
||||
public void setConnector(Connector connector) {
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL configuration.
|
||||
*/
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
*/
|
||||
private String bundle;
|
||||
|
||||
public String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported factory types.
|
||||
*/
|
||||
public enum Connector {
|
||||
|
||||
/**
|
||||
* Reactor-Netty.
|
||||
*/
|
||||
REACTOR(ClientHttpConnectorBuilder::reactor),
|
||||
|
||||
/**
|
||||
* Jetty's HttpClient.
|
||||
*/
|
||||
JETTY(ClientHttpConnectorBuilder::jetty),
|
||||
|
||||
/**
|
||||
* Apache HttpComponents HttpClient.
|
||||
*/
|
||||
HTTP_COMPONENTS(ClientHttpConnectorBuilder::httpComponents),
|
||||
|
||||
/**
|
||||
* Java's HttpClient.
|
||||
*/
|
||||
JDK(ClientHttpConnectorBuilder::jdk);
|
||||
|
||||
private final Supplier<ClientHttpConnectorBuilder<?>> builderSupplier;
|
||||
|
||||
Connector(Supplier<ClientHttpConnectorBuilder<?>> builderSupplier) {
|
||||
this.builderSupplier = builderSupplier;
|
||||
}
|
||||
|
||||
ClientHttpConnectorBuilder<?> builder() {
|
||||
return this.builderSupplier.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.boot.reactor.netty.autoconfigure.ReactorNettyConfigurations;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for
|
||||
* {@link ClientHttpConnectorBuilder} and {@link ClientHttpConnectorSettings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = SslAutoConfiguration.class)
|
||||
@ConditionalOnClass({ ClientHttpConnector.class, Mono.class })
|
||||
@Conditional(ConditionalOnClientHttpConnectorBuilderDetection.class)
|
||||
@EnableConfigurationProperties(HttpReactiveClientProperties.class)
|
||||
public class ClientHttpConnectorAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
private final ClientHttpConnectors connectors;
|
||||
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
ClientHttpConnectorAutoConfiguration(ObjectProvider<SslBundles> sslBundles,
|
||||
HttpReactiveClientProperties properties) {
|
||||
this.connectors = new ClientHttpConnectors(sslBundles, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.beanClassLoader = classLoader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder(
|
||||
ObjectProvider<ClientHttpConnectorBuilderCustomizer<?>> clientHttpConnectorBuilderCustomizers) {
|
||||
ClientHttpConnectorBuilder<?> builder = this.connectors.builder(this.beanClassLoader);
|
||||
return customize(builder, clientHttpConnectorBuilderCustomizers.orderedStream().toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ClientHttpConnectorBuilder<?> customize(ClientHttpConnectorBuilder<?> builder,
|
||||
List<ClientHttpConnectorBuilderCustomizer<?>> customizers) {
|
||||
ClientHttpConnectorBuilder<?>[] builderReference = { builder };
|
||||
LambdaSafe.callbacks(ClientHttpConnectorBuilderCustomizer.class, customizers, builderReference[0])
|
||||
.invoke((customizer) -> builderReference[0] = customizer.customize(builderReference[0]));
|
||||
return builderReference[0];
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnectorSettings clientHttpConnectorSettings() {
|
||||
return this.connectors.settings();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnector clientHttpConnector(ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder,
|
||||
ClientHttpConnectorSettings clientHttpRequestFactorySettings) {
|
||||
return clientHttpConnectorBuilder.build(clientHttpRequestFactorySettings);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(reactor.netty.http.client.HttpClient.class)
|
||||
@Import(ReactorNettyConfigurations.ReactorResourceFactoryConfiguration.class)
|
||||
static class ReactorNetty {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
|
||||
/**
|
||||
* Customizer that can be used to modify the auto-configured
|
||||
* {@link ClientHttpConnectorBuilder} when its type matches.
|
||||
*
|
||||
* @param <B> the builder type
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public interface ClientHttpConnectorBuilderCustomizer<B extends ClientHttpConnectorBuilder<?>> {
|
||||
|
||||
/**
|
||||
* Customize the given builder.
|
||||
* @param builder the builder to customize
|
||||
* @return the customized builder
|
||||
*/
|
||||
B customize(B builder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.boot.http.client.reactive.autoconfigure.AbstractClientHttpConnectorProperties.Connector;
|
||||
import org.springframework.boot.http.client.reactive.autoconfigure.AbstractClientHttpConnectorProperties.Ssl;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Helper class to create {@link ClientHttpConnectorBuilder} and
|
||||
* {@link ClientHttpConnectorSettings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public final class ClientHttpConnectors {
|
||||
|
||||
private final ObjectFactory<SslBundles> sslBundles;
|
||||
|
||||
private final AbstractClientHttpConnectorProperties[] orderedProperties;
|
||||
|
||||
public ClientHttpConnectors(ObjectFactory<SslBundles> sslBundles,
|
||||
AbstractClientHttpConnectorProperties... orderedProperties) {
|
||||
this.sslBundles = sslBundles;
|
||||
this.orderedProperties = orderedProperties;
|
||||
}
|
||||
|
||||
public ClientHttpConnectorBuilder<?> builder(ClassLoader classLoader) {
|
||||
Connector connector = getProperty(AbstractClientHttpConnectorProperties::getConnector);
|
||||
return (connector != null) ? connector.builder() : ClientHttpConnectorBuilder.detect(classLoader);
|
||||
}
|
||||
|
||||
public ClientHttpConnectorSettings settings() {
|
||||
HttpRedirects redirects = getProperty(AbstractClientHttpConnectorProperties::getRedirects);
|
||||
Duration connectTimeout = getProperty(AbstractClientHttpConnectorProperties::getConnectTimeout);
|
||||
Duration readTimeout = getProperty(AbstractClientHttpConnectorProperties::getReadTimeout);
|
||||
String sslBundleName = getProperty(AbstractClientHttpConnectorProperties::getSsl, Ssl::getBundle,
|
||||
StringUtils::hasText);
|
||||
SslBundle sslBundle = (StringUtils.hasLength(sslBundleName))
|
||||
? this.sslBundles.getObject().getBundle(sslBundleName) : null;
|
||||
return new ClientHttpConnectorSettings(redirects, connectTimeout, readTimeout, sslBundle);
|
||||
}
|
||||
|
||||
private <T> T getProperty(Function<AbstractClientHttpConnectorProperties, T> accessor) {
|
||||
return getProperty(accessor, Function.identity(), Objects::nonNull);
|
||||
}
|
||||
|
||||
private <P, T> T getProperty(Function<AbstractClientHttpConnectorProperties, P> accessor, Function<P, T> extractor,
|
||||
Predicate<T> predicate) {
|
||||
for (AbstractClientHttpConnectorProperties properties : this.orderedProperties) {
|
||||
if (properties != null) {
|
||||
P value = accessor.apply(properties);
|
||||
T extracted = (value != null) ? extractor.apply(value) : null;
|
||||
if (predicate.test(extracted)) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* {@link Condition} that checks that {@link ClientHttpConnectorBuilder} can be detected.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ConditionalOnClientHttpConnectorBuilderDetection extends SpringBootCondition {
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
try {
|
||||
ClientHttpConnectorBuilder.detect(context.getClassLoader());
|
||||
return ConditionOutcome.match("Detected ClientHttpConnectorBuilder");
|
||||
}
|
||||
catch (IllegalStateException ex) {
|
||||
return ConditionOutcome.noMatch("Unable to detect ClientHttpConnectorBuilder");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,42 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
|
||||
/**
|
||||
* Properties for HTTP Service clients.
|
||||
* {@link ConfigurationProperties @ConfigurationProperties} to configure settings that
|
||||
* apply to Spring's reactive client HTTP connectors.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
* @see ClientHttpConnectorSettings
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.client.service")
|
||||
public class HttpClientServiceProperties extends AbstractHttpClientServiceProperties {
|
||||
|
||||
/**
|
||||
* Group settings.
|
||||
*/
|
||||
private Map<String, Group> group = new LinkedHashMap<>();
|
||||
|
||||
public Map<String, Group> getGroup() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
public void setGroup(Map<String, Group> group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties for a single HTTP Service client group.
|
||||
*/
|
||||
public static class Group extends AbstractHttpClientServiceProperties {
|
||||
|
||||
}
|
||||
@ConfigurationProperties("spring.http.reactiveclient")
|
||||
public class HttpReactiveClientProperties extends AbstractClientHttpConnectorProperties {
|
||||
|
||||
}
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Observation integration for RestClient and RestTemplate.
|
||||
* Auto-configuration for client-side reactive HTTP.
|
||||
*/
|
||||
package org.springframework.boot.http.client.rest.actuate.observation;
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-Configuration for Spring's Blocking HTTP Service Interface Clients.
|
||||
* Client-side reactive HTTP support classes.
|
||||
*/
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.actuate.observation;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
/**
|
||||
* {@link RestClientCustomizer} that configures the {@link Builder RestClient builder} to
|
||||
* record request observations.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class ObservationRestClientCustomizer implements RestClientCustomizer {
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
private final ClientRequestObservationConvention observationConvention;
|
||||
|
||||
/**
|
||||
* Create a new {@link ObservationRestClientCustomizer}.
|
||||
* @param observationRegistry the observation registry
|
||||
* @param observationConvention the observation convention
|
||||
*/
|
||||
public ObservationRestClientCustomizer(ObservationRegistry observationRegistry,
|
||||
ClientRequestObservationConvention observationConvention) {
|
||||
Assert.notNull(observationConvention, "'observationConvention' must not be null");
|
||||
Assert.notNull(observationRegistry, "'observationRegistry' must not be null");
|
||||
this.observationRegistry = observationRegistry;
|
||||
this.observationConvention = observationConvention;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Builder restClientBuilder) {
|
||||
restClientBuilder.observationRegistry(this.observationRegistry);
|
||||
restClientBuilder.observationConvention(this.observationConvention);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.actuate.observation;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
||||
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* {@link RestTemplateCustomizer} that configures the {@link RestTemplate} to record
|
||||
* request observations.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class ObservationRestTemplateCustomizer implements RestTemplateCustomizer {
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
private final ClientRequestObservationConvention observationConvention;
|
||||
|
||||
/**
|
||||
* Create a new {@code ObservationRestTemplateCustomizer}.
|
||||
* @param observationConvention the observation convention
|
||||
* @param observationRegistry the observation registry
|
||||
*/
|
||||
public ObservationRestTemplateCustomizer(ObservationRegistry observationRegistry,
|
||||
ClientRequestObservationConvention observationConvention) {
|
||||
this.observationConvention = observationConvention;
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(RestTemplate restTemplate) {
|
||||
restTemplate.setObservationConvention(this.observationConvention);
|
||||
restTemplate.setObservationRegistry(this.observationRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* An auto-configured {@link RestClientSsl} implementation.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dmytro Nosan
|
||||
*/
|
||||
class AutoConfiguredRestClientSsl implements RestClientSsl {
|
||||
|
||||
private final ClientHttpRequestFactoryBuilder<?> builder;
|
||||
|
||||
private final ClientHttpRequestFactorySettings settings;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
AutoConfiguredRestClientSsl(ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder,
|
||||
ClientHttpRequestFactorySettings clientHttpRequestFactorySettings, SslBundles sslBundles) {
|
||||
this.builder = clientHttpRequestFactoryBuilder;
|
||||
this.settings = clientHttpRequestFactorySettings;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<RestClient.Builder> fromBundle(String bundleName) {
|
||||
return fromBundle(this.sslBundles.getBundle(bundleName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<RestClient.Builder> fromBundle(SslBundle bundle) {
|
||||
return (builder) -> builder.requestFactory(requestFactory(bundle));
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactory requestFactory(SslBundle bundle) {
|
||||
return this.builder.build(this.settings.withSslBundle(bundle));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* {@link RestClientCustomizer} to apply {@link HttpMessageConverter
|
||||
* HttpMessageConverters}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer {
|
||||
|
||||
private final Iterable<? extends HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
public HttpMessageConvertersRestClientCustomizer(HttpMessageConverter<?>... messageConverters) {
|
||||
Assert.notNull(messageConverters, "'messageConverters' must not be null");
|
||||
this.messageConverters = Arrays.asList(messageConverters);
|
||||
}
|
||||
|
||||
HttpMessageConvertersRestClientCustomizer(HttpMessageConverters messageConverters) {
|
||||
this.messageConverters = messageConverters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(RestClient.Builder restClientBuilder) {
|
||||
restClientBuilder.messageConverters(this::configureMessageConverters);
|
||||
}
|
||||
|
||||
private void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
|
||||
if (this.messageConverters != null) {
|
||||
messageConverters.clear();
|
||||
this.messageConverters.forEach(messageConverters::add);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
|
||||
/**
|
||||
* {@link SpringBootCondition} that applies only when running in a non-reactive web
|
||||
* application.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NotReactiveWebApplicationCondition extends NoneNestedConditions {
|
||||
|
||||
NotReactiveWebApplicationCondition() {
|
||||
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||
}
|
||||
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
private static final class ReactiveWebApplication {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.thread.Threading;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
|
||||
/**
|
||||
* {@link SpringBootCondition} that applies when running in a non-reactive web application
|
||||
* or virtual threads are enabled.
|
||||
*
|
||||
* @author Dmitry Sulman
|
||||
*/
|
||||
class NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition extends AnyNestedCondition {
|
||||
|
||||
NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@Conditional(NotReactiveWebApplicationCondition.class)
|
||||
private static final class NotReactiveWebApplication {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnThreading(Threading.VIRTUAL)
|
||||
@ConditionalOnBean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
|
||||
private static final class VirtualThreadsExecutorEnabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link RestClient}.
|
||||
* <p>
|
||||
* This will produce a {@link Builder RestClient.Builder} bean with the {@code prototype}
|
||||
* scope, meaning each injection point will receive a newly cloned instance of the
|
||||
* builder.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(
|
||||
after = { HttpClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class, SslAutoConfiguration.class })
|
||||
@ConditionalOnClass(RestClient.class)
|
||||
@Conditional(NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.class)
|
||||
public class RestClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(RestClientSsl.class)
|
||||
@ConditionalOnBean(SslBundles.class)
|
||||
AutoConfiguredRestClientSsl restClientSsl(
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings, SslBundles sslBundles) {
|
||||
return new AutoConfiguredRestClientSsl(
|
||||
clientHttpRequestFactoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect),
|
||||
clientHttpRequestFactorySettings.getIfAvailable(ClientHttpRequestFactorySettings::defaults),
|
||||
sslBundles);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RestClientBuilderConfigurer restClientBuilderConfigurer(
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
|
||||
ObjectProvider<RestClientCustomizer> customizerProvider) {
|
||||
return new RestClientBuilderConfigurer(
|
||||
clientHttpRequestFactoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect),
|
||||
clientHttpRequestFactorySettings.getIfAvailable(ClientHttpRequestFactorySettings::defaults),
|
||||
customizerProvider.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@ConditionalOnMissingBean
|
||||
RestClient.Builder restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) {
|
||||
return restClientBuilderConfigurer.configure(RestClient.builder());
|
||||
}
|
||||
|
||||
@ConditionalOnClass(HttpMessageConverters.class)
|
||||
static class HttpMessageConvertersConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer(
|
||||
ObjectProvider<HttpMessageConverters> messageConverters) {
|
||||
return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
/**
|
||||
* Configure {@link Builder RestClient.Builder} with sensible defaults.
|
||||
* <p>
|
||||
* Can be injected into application code and used to define a custom
|
||||
* {@code RestClient.Builder} whose configuration is based upon that produced by
|
||||
* auto-configuration.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class RestClientBuilderConfigurer {
|
||||
|
||||
private final ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder;
|
||||
|
||||
private final ClientHttpRequestFactorySettings requestFactorySettings;
|
||||
|
||||
private final List<RestClientCustomizer> customizers;
|
||||
|
||||
public RestClientBuilderConfigurer() {
|
||||
this(ClientHttpRequestFactoryBuilder.detect(), ClientHttpRequestFactorySettings.defaults(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
RestClientBuilderConfigurer(ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder,
|
||||
ClientHttpRequestFactorySettings requestFactorySettings, List<RestClientCustomizer> customizers) {
|
||||
this.requestFactoryBuilder = requestFactoryBuilder;
|
||||
this.requestFactorySettings = requestFactorySettings;
|
||||
this.customizers = customizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the specified {@link Builder RestClient.Builder}. The builder can be
|
||||
* further tuned and default settings can be overridden.
|
||||
* @param builder the {@link Builder RestClient.Builder} instance to configure
|
||||
* @return the configured builder
|
||||
*/
|
||||
public RestClient.Builder configure(RestClient.Builder builder) {
|
||||
builder.requestFactory(this.requestFactoryBuilder.build(this.requestFactorySettings));
|
||||
applyCustomizers(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void applyCustomizers(Builder builder) {
|
||||
for (RestClientCustomizer customizer : this.customizers) {
|
||||
customizer.customize(builder);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* Interface that can be used to {@link RestClient.Builder#apply apply} SSL configuration
|
||||
* to a {@link org.springframework.web.client.RestClient.Builder RestClient.Builder}.
|
||||
* <p>
|
||||
* Typically used as follows: <pre class="code">
|
||||
* @Bean
|
||||
* public MyBean myBean(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
|
||||
* RestClient restClient = restClientBuilder.apply(ssl.fromBundle("mybundle")).build();
|
||||
* return new MyBean(restClient);
|
||||
* }
|
||||
* </pre> NOTE: Applying SSL configuration will replace any previously
|
||||
* {@link RestClient.Builder#requestFactory configured} {@link ClientHttpRequestFactory}.
|
||||
* The replacement {@link ClientHttpRequestFactory} will apply only configured
|
||||
* {@link ClientHttpRequestFactorySettings} and the appropriate {@link SslBundle}.
|
||||
* <p>
|
||||
* If you need to configure {@link ClientHttpRequestFactory} with more than just SSL
|
||||
* consider using a {@link ClientHttpRequestFactoryBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface RestClientSsl {
|
||||
|
||||
/**
|
||||
* Return a {@link Consumer} that will apply SSL configuration for the named
|
||||
* {@link SslBundle} to a {@link org.springframework.web.client.RestClient.Builder
|
||||
* RestClient.Builder}.
|
||||
* @param bundleName the name of the SSL bundle to apply
|
||||
* @return a {@link Consumer} to apply the configuration
|
||||
* @throws NoSuchSslBundleException if a bundle with the provided name does not exist
|
||||
*/
|
||||
Consumer<RestClient.Builder> fromBundle(String bundleName) throws NoSuchSslBundleException;
|
||||
|
||||
/**
|
||||
* Return a {@link Consumer} that will apply SSL configuration for the
|
||||
* {@link SslBundle} to a {@link org.springframework.web.client.RestClient.Builder
|
||||
* RestClient.Builder}.
|
||||
* @param bundle the SSL bundle to apply
|
||||
* @return a {@link Consumer} to apply the configuration
|
||||
*/
|
||||
Consumer<RestClient.Builder> fromBundle(SslBundle bundle);
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
||||
import org.springframework.boot.web.client.RestTemplateRequestCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link RestTemplate} (via
|
||||
* {@link RestTemplateBuilder}).
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = HttpClientAutoConfiguration.class)
|
||||
@ConditionalOnClass({ RestTemplate.class, HttpMessageConverters.class })
|
||||
@Conditional(NotReactiveWebApplicationCondition.class)
|
||||
public class RestTemplateAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
|
||||
ObjectProvider<HttpMessageConverters> messageConverters,
|
||||
ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers,
|
||||
ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) {
|
||||
RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer();
|
||||
configurer.setRequestFactoryBuilder(clientHttpRequestFactoryBuilder.getIfAvailable());
|
||||
configurer.setRequestFactorySettings(clientHttpRequestFactorySettings.getIfAvailable());
|
||||
configurer.setHttpMessageConverters(messageConverters.getIfUnique());
|
||||
configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().toList());
|
||||
configurer.setRestTemplateRequestCustomizers(restTemplateRequestCustomizers.orderedStream().toList());
|
||||
return configurer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@ConditionalOnMissingBean
|
||||
public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) {
|
||||
return restTemplateBuilderConfigurer.configure(new RestTemplateBuilder());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
||||
import org.springframework.boot.web.client.RestTemplateRequestCustomizer;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Configure {@link RestTemplateBuilder} with sensible defaults.
|
||||
* <p>
|
||||
* Can be injected into application code and used to define a custom
|
||||
* {@code RestTemplateBuilder} whose configuration is based upon that produced by
|
||||
* auto-configuration.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public final class RestTemplateBuilderConfigurer {
|
||||
|
||||
private ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder;
|
||||
|
||||
private ClientHttpRequestFactorySettings requestFactorySettings;
|
||||
|
||||
private HttpMessageConverters httpMessageConverters;
|
||||
|
||||
private List<RestTemplateCustomizer> restTemplateCustomizers;
|
||||
|
||||
private List<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers;
|
||||
|
||||
void setRequestFactoryBuilder(ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder) {
|
||||
this.requestFactoryBuilder = requestFactoryBuilder;
|
||||
}
|
||||
|
||||
void setRequestFactorySettings(ClientHttpRequestFactorySettings requestFactorySettings) {
|
||||
this.requestFactorySettings = requestFactorySettings;
|
||||
}
|
||||
|
||||
void setHttpMessageConverters(HttpMessageConverters httpMessageConverters) {
|
||||
this.httpMessageConverters = httpMessageConverters;
|
||||
}
|
||||
|
||||
void setRestTemplateCustomizers(List<RestTemplateCustomizer> restTemplateCustomizers) {
|
||||
this.restTemplateCustomizers = restTemplateCustomizers;
|
||||
}
|
||||
|
||||
void setRestTemplateRequestCustomizers(List<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) {
|
||||
this.restTemplateRequestCustomizers = restTemplateRequestCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the specified {@link RestTemplateBuilder}. The builder can be further
|
||||
* tuned and default settings can be overridden.
|
||||
* @param builder the {@link RestTemplateBuilder} instance to configure
|
||||
* @return the configured builder
|
||||
*/
|
||||
public RestTemplateBuilder configure(RestTemplateBuilder builder) {
|
||||
if (this.requestFactoryBuilder != null) {
|
||||
builder = builder.requestFactoryBuilder(this.requestFactoryBuilder);
|
||||
}
|
||||
if (this.requestFactorySettings != null) {
|
||||
builder = builder.requestFactorySettings(this.requestFactorySettings);
|
||||
}
|
||||
if (this.httpMessageConverters != null) {
|
||||
builder = builder.messageConverters(this.httpMessageConverters.getConverters());
|
||||
}
|
||||
builder = addCustomizers(builder, this.restTemplateCustomizers, RestTemplateBuilder::customizers);
|
||||
builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private <T> RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, List<T> customizers,
|
||||
BiFunction<RestTemplateBuilder, Collection<T>, RestTemplateBuilder> method) {
|
||||
if (!ObjectUtils.isEmpty(customizers)) {
|
||||
return method.apply(builder, customizers);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.http.client.autoconfigure.AbstractHttpRequestFactoryProperties;
|
||||
|
||||
/**
|
||||
* {@link AbstractHttpRequestFactoryProperties} for HTTP Service clients.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public abstract class AbstractHttpClientServiceProperties extends AbstractHttpRequestFactoryProperties {
|
||||
|
||||
/**
|
||||
* Base url to set in the underlying HTTP client group. By default, set to
|
||||
* {@code null}.
|
||||
*/
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* Default request headers for interface client group. By default, set to empty
|
||||
* {@link Map}.
|
||||
*/
|
||||
private Map<String, List<String>> defaultHeader = new LinkedHashMap<>();
|
||||
|
||||
public String getBaseUrl() {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getDefaultHeader() {
|
||||
return this.defaultHeader;
|
||||
}
|
||||
|
||||
public void setDefaultHeader(Map<String, List<String>> defaultHeaders) {
|
||||
this.defaultHeader = defaultHeaders;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientProperties;
|
||||
import org.springframework.boot.http.client.rest.autoconfigure.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.web.client.support.RestClientAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
|
||||
/**
|
||||
* AutoConfiguration for Spring HTTP Service clients.
|
||||
* <p>
|
||||
* This will result in the creation of blocking HTTP Service client beans defined by
|
||||
* {@link ImportHttpServices @ImportHttpServices} annotations.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class })
|
||||
@ConditionalOnClass(RestClientAdapter.class)
|
||||
@ConditionalOnBean(HttpServiceProxyRegistry.class)
|
||||
@Conditional(NotReactiveWebApplicationCondition.class)
|
||||
@EnableConfigurationProperties(HttpClientServiceProperties.class)
|
||||
public class HttpServiceClientAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
HttpServiceClientAutoConfiguration() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.beanClassLoader = classLoader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestClientPropertiesHttpServiceGroupConfigurer restClientPropertiesHttpServiceGroupConfigurer(
|
||||
ObjectProvider<SslBundles> sslBundles, ObjectProvider<HttpClientProperties> httpClientProperties,
|
||||
HttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings) {
|
||||
return new RestClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
|
||||
httpClientProperties.getIfAvailable(), serviceProperties, clientFactoryBuilder,
|
||||
clientHttpRequestFactorySettings);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestClientCustomizerHttpServiceGroupConfigurer restClientCustomizerHttpServiceGroupConfigurer(
|
||||
ObjectProvider<RestClientCustomizer> customizers) {
|
||||
return new RestClientCustomizerHttpServiceGroupConfigurer(customizers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
|
||||
/**
|
||||
* {@link SpringBootCondition} that applies only when running in a non-reactive web
|
||||
* application.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NotReactiveWebApplicationCondition extends NoneNestedConditions {
|
||||
|
||||
NotReactiveWebApplicationCondition() {
|
||||
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||
}
|
||||
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
private static final class ReactiveWebApplication {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup;
|
||||
|
||||
/**
|
||||
* A {@link RestClientHttpServiceGroupConfigurer} to apply auto-configured
|
||||
* {@link RestClientCustomizer} beans to the group's {@link RestClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class RestClientCustomizerHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer {
|
||||
|
||||
/**
|
||||
* Allow user defined configurers to apply before / after ours.
|
||||
*/
|
||||
private static final int ORDER = 0;
|
||||
|
||||
private final ObjectProvider<RestClientCustomizer> customizers;
|
||||
|
||||
RestClientCustomizerHttpServiceGroupConfigurer(ObjectProvider<RestClientCustomizer> customizers) {
|
||||
this.customizers = customizers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureGroups(Groups<RestClient.Builder> groups) {
|
||||
groups.forEachClient(this::configureClient);
|
||||
}
|
||||
|
||||
private void configureClient(HttpServiceGroup group, RestClient.Builder builder) {
|
||||
this.customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.autoconfigure.ClientHttpRequestFactories;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup;
|
||||
|
||||
/**
|
||||
* A {@link RestClientHttpServiceGroupConfigurer} that configures the group and its
|
||||
* underlying {@link RestClient} using {@link HttpClientProperties}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class RestClientPropertiesHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer {
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
private final ObjectProvider<SslBundles> sslBundles;
|
||||
|
||||
private final HttpClientProperties clientProperties;
|
||||
|
||||
private final HttpClientServiceProperties serviceProperties;
|
||||
|
||||
private final ObjectProvider<ClientHttpRequestFactoryBuilder<?>> requestFactoryBuilder;
|
||||
|
||||
private final ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings;
|
||||
|
||||
RestClientPropertiesHttpServiceGroupConfigurer(ClassLoader classLoader, ObjectProvider<SslBundles> sslBundles,
|
||||
HttpClientProperties clientProperties, HttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> requestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings) {
|
||||
this.classLoader = classLoader;
|
||||
this.sslBundles = sslBundles;
|
||||
this.clientProperties = clientProperties;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.requestFactoryBuilder = requestFactoryBuilder;
|
||||
this.requestFactorySettings = requestFactorySettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureGroups(Groups<RestClient.Builder> groups) {
|
||||
groups.forEachClient(this::configureClient);
|
||||
}
|
||||
|
||||
private void configureClient(HttpServiceGroup group, RestClient.Builder builder) {
|
||||
HttpClientServiceProperties.Group groupProperties = this.serviceProperties.getGroup().get(group.name());
|
||||
builder.requestFactory(getRequestFactory(groupProperties));
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(this.serviceProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(this.serviceProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
if (groupProperties != null) {
|
||||
map.from(groupProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
|
||||
map.from(groupProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
private Consumer<HttpHeaders> putAllHeaders(Map<String, List<String>> defaultHeaders) {
|
||||
return (httpHeaders) -> httpHeaders.putAll(defaultHeaders);
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactory getRequestFactory(HttpClientServiceProperties.Group groupProperties) {
|
||||
ClientHttpRequestFactories factories = new ClientHttpRequestFactories(this.sslBundles, groupProperties,
|
||||
this.serviceProperties, this.clientProperties);
|
||||
ClientHttpRequestFactoryBuilder<?> builder = this.requestFactoryBuilder
|
||||
.getIfAvailable(() -> factories.builder(this.classLoader));
|
||||
ClientHttpRequestFactorySettings settings = this.requestFactorySettings.getIfAvailable(factories::settings);
|
||||
return builder.build(settings);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.boot.http.client.ClientHttpRequestFactoryRuntimeHints
|
||||
@@ -1,3 +1,2 @@
|
||||
org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration
|
||||
org.springframework.boot.http.client.rest.autoconfigure.RestClientAutoConfiguration
|
||||
org.springframework.boot.http.client.rest.autoconfigure.RestTemplateAutoConfiguration
|
||||
org.springframework.boot.http.client.reactive.autoconfigure.ClientHttpConnectorAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.SslOptions;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.tomcat.servlet.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.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Base class for {@link ClientHttpRequestFactoryBuilder} tests.
|
||||
*
|
||||
* @param <T> The {@link ClientHttpRequestFactory} type
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
abstract class AbstractClientHttpRequestFactoryBuilderTests<T extends ClientHttpRequestFactory> {
|
||||
|
||||
private static final Function<HttpMethod, HttpStatus> ALWAYS_FOUND = (method) -> HttpStatus.FOUND;
|
||||
|
||||
private final Class<T> requestFactoryType;
|
||||
|
||||
private final ClientHttpRequestFactoryBuilder<T> builder;
|
||||
|
||||
AbstractClientHttpRequestFactoryBuilderTests(Class<T> requestFactoryType,
|
||||
ClientHttpRequestFactoryBuilder<T> builder) {
|
||||
this.requestFactoryType = requestFactoryType;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReturnsRequestFactoryOfExpectedType() {
|
||||
T requestFactory = this.builder.build();
|
||||
assertThat(requestFactory).isInstanceOf(this.requestFactoryType);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHasConnectTimeout() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(60));
|
||||
T requestFactory = this.builder.build(settings);
|
||||
assertThat(connectTimeout(requestFactory)).isEqualTo(Duration.ofSeconds(60).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHadReadTimeout() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withReadTimeout(Duration.ofSeconds(120));
|
||||
T requestFactory = this.builder.build(settings);
|
||||
assertThat(readTimeout(requestFactory)).isEqualTo(Duration.ofSeconds(120).toMillis());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@WithPackageResources("test.jks")
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@WithPackageResources("test.jks")
|
||||
@ValueSource(strings = { "GET", "POST" })
|
||||
void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
webServerFactory.setSsl(ssl("TLS_AES_128_GCM_SHA256"));
|
||||
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 requestFactory = this.builder.build(ClientHttpRequestFactorySettings
|
||||
.ofSslBundle(sslBundle(SslOptions.of(Set.of("TLS_AES_256_GCM_SHA384"), null))));
|
||||
ClientHttpRequest secureRequest = request(requestFactory, uri, httpMethod);
|
||||
assertThatExceptionOfType(SSLHandshakeException.class).isThrownBy(() -> secureRequest.execute().getBody());
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectDefault(String httpMethod) throws Exception {
|
||||
testRedirect(null, HttpMethod.valueOf(httpMethod), this::getExpectedRedirect);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectFollow(String httpMethod) throws Exception {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.FOLLOW);
|
||||
testRedirect(settings, HttpMethod.valueOf(httpMethod), this::getExpectedRedirect);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectDontFollow(String httpMethod) throws Exception {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
testRedirect(settings, HttpMethod.valueOf(httpMethod), ALWAYS_FOUND);
|
||||
}
|
||||
|
||||
protected final void testRedirect(ClientHttpRequestFactorySettings settings, HttpMethod httpMethod,
|
||||
Function<HttpMethod, HttpStatus> expectedStatusForMethod) throws URISyntaxException, IOException {
|
||||
HttpStatus expectedStatus = expectedStatusForMethod.apply(httpMethod);
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect");
|
||||
ClientHttpRequestFactory requestFactory = this.builder.build(settings);
|
||||
ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
|
||||
ClientHttpResponse response = request.execute();
|
||||
assertThat(response.getStatusCode()).isEqualTo(expectedStatus);
|
||||
if (expectedStatus == HttpStatus.OK) {
|
||||
assertThat(response.getBody()).asString(StandardCharsets.UTF_8).contains("request to /redirected");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private ClientHttpRequest request(ClientHttpRequestFactory factory, URI uri, String method) throws IOException {
|
||||
return factory.createRequest(uri, HttpMethod.valueOf(method));
|
||||
}
|
||||
|
||||
private Ssl ssl(String... ciphers) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setClientAuth(ClientAuth.NEED);
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setTrustStore("classpath:test.jks");
|
||||
if (ciphers.length > 0) {
|
||||
ssl.setCiphers(ciphers);
|
||||
}
|
||||
return ssl;
|
||||
}
|
||||
|
||||
protected final SslBundle sslBundle() {
|
||||
return sslBundle(SslOptions.NONE);
|
||||
}
|
||||
|
||||
protected final SslBundle sslBundle(SslOptions sslOptions) {
|
||||
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks");
|
||||
JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails);
|
||||
return SslBundle.of(stores, SslBundleKey.of("password"), sslOptions);
|
||||
}
|
||||
|
||||
protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) {
|
||||
return HttpStatus.OK;
|
||||
}
|
||||
|
||||
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 {
|
||||
if ("/redirect".equals(req.getRequestURI())) {
|
||||
res.sendRedirect("/redirected");
|
||||
return;
|
||||
}
|
||||
res.getWriter().println("Received " + req.getMethod() + " request to " + req.getRequestURI());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpRequestFactoryBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpRequestFactoryBuilderTests {
|
||||
|
||||
@Test
|
||||
void withCustomizerAppliesCustomizers() {
|
||||
ClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> builder = (
|
||||
settings) -> new JettyClientHttpRequestFactory();
|
||||
builder = builder.withCustomizer(this::setJettyReadTimeout);
|
||||
JettyClientHttpRequestFactory factory = builder.build(null);
|
||||
assertThat(factory).extracting("readTimeout").isEqualTo(5000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizersAppliesCustomizers() {
|
||||
ClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> builder = (
|
||||
settings) -> new JettyClientHttpRequestFactory();
|
||||
builder = builder.withCustomizers(List.of(this::setJettyReadTimeout));
|
||||
JettyClientHttpRequestFactory factory = builder.build(null);
|
||||
assertThat(factory).extracting("readTimeout").isEqualTo(5000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpComponentsReturnsHttpComponentsFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.httpComponents())
|
||||
.isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyReturnsJettyFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.jetty()).isInstanceOf(JettyClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reactorReturnsReactorFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.reactor())
|
||||
.isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdkReturnsJdkFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.jdk()).isInstanceOf(JdkClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleReturnsSimpleFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.simple()).isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenExactlyClientHttpRequestFactoryTypeThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ClientHttpRequestFactoryBuilder.of(ClientHttpRequestFactory.class))
|
||||
.withMessage("'requestFactoryType' must be an implementation of ClientHttpRequestFactory");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenSimpleFactoryReturnsSimpleFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.of(SimpleClientHttpRequestFactory.class))
|
||||
.isInstanceOf(SimpleClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenHttpComponentsFactoryReturnsHttpComponentsFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.of(HttpComponentsClientHttpRequestFactory.class))
|
||||
.isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenReactorFactoryReturnsReactorFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.of(ReactorClientHttpRequestFactory.class))
|
||||
.isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenJdkFactoryReturnsJdkFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.of(JdkClientHttpRequestFactory.class))
|
||||
.isInstanceOf(JdkClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenUnknownTypeReturnsReflectiveFactoryBuilder() {
|
||||
ClientHttpRequestFactoryBuilder<TestClientHttpRequestFactory> builder = ClientHttpRequestFactoryBuilder
|
||||
.of(TestClientHttpRequestFactory.class);
|
||||
assertThat(builder).isInstanceOf(ReflectiveComponentsClientHttpRequestFactoryBuilder.class);
|
||||
assertThat(builder.build(null)).isInstanceOf(TestClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithSupplierWhenSupplierIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ClientHttpRequestFactoryBuilder.of((Supplier<ClientHttpRequestFactory>) null))
|
||||
.withMessage("'requestFactorySupplier' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithSupplierReturnsReflectiveFactoryBuilder() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.of(SimpleClientHttpRequestFactory::new))
|
||||
.isInstanceOf(ReflectiveComponentsClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectWhenHttpComponents() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.detect())
|
||||
.isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions("httpclient5-*.jar")
|
||||
void detectWhenJetty() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.detect()).isInstanceOf(JettyClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
|
||||
void detectWhenReactor() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.detect()).isInstanceOf(ReactorClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "reactor-netty-http-*.jar" })
|
||||
void detectWhenJdk() {
|
||||
assertThat(ClientHttpRequestFactoryBuilder.detect()).isInstanceOf(JdkClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
private void setJettyReadTimeout(JettyClientHttpRequestFactory factory) {
|
||||
factory.setReadTimeout(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
public static class TestClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.ReflectionHintsPredicates;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpRequestFactoryRuntimeHints}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ClientHttpRequestFactoryRuntimeHintsTests {
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
|
||||
Field requestFactoryField = ReflectionUtils.findField(AbstractClientHttpRequestFactoryWrapper.class,
|
||||
"requestFactory");
|
||||
assertThat(requestFactoryField).isNotNull();
|
||||
assertThat(reflection.onFieldAccess(requestFactoryField)).accepts(hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHttpComponentHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(HttpComponentsClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
|
||||
.accepts(hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterJettyClientHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(JettyClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
|
||||
.accepts(hints);
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(JettyClientHttpRequestFactory.class, "setReadTimeout", long.class)))
|
||||
.accepts(hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterReactorHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(ReactorClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
|
||||
.accepts(hints);
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(ReactorClientHttpRequestFactory.class, "setReadTimeout", long.class)))
|
||||
.accepts(hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterSimpleHttpHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new ClientHttpRequestFactoryRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(SimpleClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
|
||||
.accepts(hints);
|
||||
assertThat(reflection
|
||||
.onMethodInvocation(method(SimpleClientHttpRequestFactory.class, "setReadTimeout", int.class)))
|
||||
.accepts(hints);
|
||||
}
|
||||
|
||||
private static Method method(Class<?> target, String name, Class<?>... parameterTypes) {
|
||||
Method method = ReflectionUtils.findMethod(target, name, parameterTypes);
|
||||
assertThat(method).isNotNull();
|
||||
return method;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.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 defaults() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults();
|
||||
assertThat(settings.redirects()).isEqualTo(HttpRedirects.FOLLOW_WHEN_POSSIBLE);
|
||||
assertThat(settings.connectTimeout()).isNull();
|
||||
assertThat(settings.readTimeout()).isNull();
|
||||
assertThat(settings.sslBundle()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithNullsUsesDefaults() {
|
||||
ClientHttpRequestFactorySettings settings = new ClientHttpRequestFactorySettings(null, null, null, null);
|
||||
assertThat(settings.redirects()).isEqualTo(HttpRedirects.FOLLOW_WHEN_POSSIBLE);
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withRedirectsReturnsInstanceWithUpdatedRedirect() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
assertThat(settings.redirects()).isEqualTo(HttpRedirects.DONT_FOLLOW);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.HttpRoute;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
|
||||
import org.apache.hc.core5.function.Resolver;
|
||||
import org.apache.hc.core5.http.io.SocketConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder} and
|
||||
* {@link HttpComponentsHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class HttpComponentsClientHttpRequestFactoryBuilderTests
|
||||
extends AbstractClientHttpRequestFactoryBuilderTests<HttpComponentsClientHttpRequestFactory> {
|
||||
|
||||
HttpComponentsClientHttpRequestFactoryBuilderTests() {
|
||||
super(HttpComponentsClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.httpComponents());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClientBuilder> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClientBuilder> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<SocketConfig.Builder> socketConfigCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<SocketConfig.Builder> socketConfigCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<RequestConfig.Builder> defaultRequestConfigCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<RequestConfig.Builder> defaultRequestConfigCustomizer1 = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.httpComponents()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.withConnectionManagerCustomizer(connectionManagerCustomizer)
|
||||
.withSocketConfigCustomizer(socketConfigCustomizer)
|
||||
.withSocketConfigCustomizer(socketConfigCustomizer1)
|
||||
.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer)
|
||||
.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer1)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
connectionManagerCustomizer.assertCalled();
|
||||
socketConfigCustomizer.assertCalled();
|
||||
socketConfigCustomizer1.assertCalled();
|
||||
defaultRequestConfigCustomizer.assertCalled();
|
||||
defaultRequestConfigCustomizer1.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void withTlsSocketStrategyFactory() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.ofSslBundle(sslBundle());
|
||||
List<SslBundle> bundles = new ArrayList<>();
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory = (bundle) -> {
|
||||
bundles.add(bundle);
|
||||
return (socket, target, port, attachment, context) -> null;
|
||||
};
|
||||
ClientHttpRequestFactoryBuilder.httpComponents()
|
||||
.withTlsSocketStrategyFactory(tlsSocketStrategyFactory)
|
||||
.build(settings);
|
||||
assertThat(bundles).contains(settings.sslBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
|
||||
return (long) ReflectionTestUtils.getField(requestFactory, "connectTimeout");
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected long readTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
|
||||
HttpClient httpClient = requestFactory.getHttpClient();
|
||||
Object connectionManager = ReflectionTestUtils.getField(httpClient, "connManager");
|
||||
SocketConfig socketConfig = ((Resolver<HttpRoute, SocketConfig>) ReflectionTestUtils.getField(connectionManager,
|
||||
"socketConfigResolver"))
|
||||
.resolve(null);
|
||||
return socketConfig.getSoTimeout().toMilliseconds();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpRequestFactoryBuilder} and {@link JdkHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkClientHttpRequestFactoryBuilderTests
|
||||
extends AbstractClientHttpRequestFactoryBuilderTests<JdkClientHttpRequestFactory> {
|
||||
|
||||
JdkClientHttpRequestFactoryBuilderTests() {
|
||||
super(JdkClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.jdk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClient.Builder> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClient.Builder> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyClientHttpRequestFactoryBuilder} and
|
||||
* {@link JettyHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JettyClientHttpRequestFactoryBuilderTests
|
||||
extends AbstractClientHttpRequestFactoryBuilderTests<JettyClientHttpRequestFactory> {
|
||||
|
||||
JettyClientHttpRequestFactoryBuilderTests() {
|
||||
super(JettyClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.jetty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClient> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClient> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClientTransport> httpClientTransportCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<ClientConnector> clientConnectorCustomizerCustomizer = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.jetty()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.withHttpClientTransportCustomizer(httpClientTransportCustomizer)
|
||||
.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
httpClientTransportCustomizer.assertCalled();
|
||||
clientConnectorCustomizerCustomizer.assertCalled();
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorClientHttpRequestFactoryBuilder} and
|
||||
* {@link ReactorHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ReactorClientHttpRequestFactoryBuilderTests
|
||||
extends AbstractClientHttpRequestFactoryBuilderTests<ReactorClientHttpRequestFactory> {
|
||||
|
||||
ReactorClientHttpRequestFactoryBuilderTests() {
|
||||
super(ReactorClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.reactor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHttpClientFactory() {
|
||||
boolean[] called = new boolean[1];
|
||||
Supplier<HttpClient> httpClientFactory = () -> {
|
||||
called[0] = true;
|
||||
return HttpClient.create();
|
||||
};
|
||||
ClientHttpRequestFactoryBuilder.reactor().withHttpClientFactory(httpClientFactory).build();
|
||||
assertThat(called).containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withReactorResourceFactory() {
|
||||
ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory());
|
||||
ClientHttpRequestFactoryBuilder.reactor().withReactorResourceFactory(resourceFactory).build();
|
||||
then(resourceFactory).should().getConnectionProvider();
|
||||
then(resourceFactory).should().getLoopResources();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
List<HttpClient> httpClients = new ArrayList<>();
|
||||
UnaryOperator<HttpClient> httpClientCustomizer1 = (httpClient) -> {
|
||||
httpClients.add(httpClient);
|
||||
return httpClient;
|
||||
};
|
||||
UnaryOperator<HttpClient> httpClientCustomizer2 = (httpClient) -> {
|
||||
httpClients.add(httpClient);
|
||||
return httpClient;
|
||||
};
|
||||
ClientHttpRequestFactoryBuilder.reactor()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.build();
|
||||
assertThat(httpClients).hasSize(2);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.BufferingClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReflectiveComponentsClientHttpRequestFactoryBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ReflectiveComponentsClientHttpRequestFactoryBuilderTests
|
||||
extends AbstractClientHttpRequestFactoryBuilderTests<ClientHttpRequestFactory> {
|
||||
|
||||
ReflectiveComponentsClientHttpRequestFactoryBuilderTests() {
|
||||
super(ClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.of(JettyClientHttpRequestFactory::new));
|
||||
}
|
||||
|
||||
@Override
|
||||
void connectWithSslBundle(String httpMethod) throws Exception {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.ofSslBundle(sslBundle());
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings))
|
||||
.withMessage("Unable to set SSL bundler using reflection");
|
||||
}
|
||||
|
||||
@Override
|
||||
void redirectFollow(String httpMethod) throws Exception {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.FOLLOW);
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings))
|
||||
.withMessage("Unable to set redirect follow using reflection");
|
||||
}
|
||||
|
||||
@Override
|
||||
void redirectDontFollow(String httpMethod) throws Exception {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofTestRequestFactory().build(settings))
|
||||
.withMessage("Unable to set redirect follow using reflection");
|
||||
}
|
||||
|
||||
@Override
|
||||
void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception {
|
||||
assertThatIllegalStateException().isThrownBy(() -> super.connectWithSslBundleAndOptionsMismatch(httpMethod))
|
||||
.withMessage("Unable to set SSL bundler using reflection");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassCreatesFactory() {
|
||||
assertThat(ofTestRequestFactory().build()).isInstanceOf(TestClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenHasConnectTimeout() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(60));
|
||||
TestClientHttpRequestFactory requestFactory = ofTestRequestFactory().build(settings);
|
||||
assertThat(requestFactory.connectTimeout).isEqualTo(Duration.ofSeconds(60).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenHasReadTimeout() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withReadTimeout(Duration.ofSeconds(90));
|
||||
TestClientHttpRequestFactory requestFactory = ofTestRequestFactory().build(settings);
|
||||
assertThat(requestFactory.readTimeout).isEqualTo(Duration.ofSeconds(90).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenUnconfigurableTypeWithConnectTimeoutThrowsException() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(60));
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofUnconfigurableRequestFactory().build(settings))
|
||||
.withMessageContaining("suitable setConnectTimeout method");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenUnconfigurableTypeWithReadTimeoutThrowsException() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withReadTimeout(Duration.ofSeconds(60));
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofUnconfigurableRequestFactory().build(settings))
|
||||
.withMessageContaining("suitable setReadTimeout method");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenDeprecatedMethodsTypeWithConnectTimeoutThrowsException() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(60));
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofDeprecatedMethodsRequestFactory().build(settings))
|
||||
.withMessageContaining("setConnectTimeout method marked as deprecated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenDeprecatedMethodsTypeWithReadTimeoutThrowsException() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withReadTimeout(Duration.ofSeconds(60));
|
||||
assertThatIllegalStateException().isThrownBy(() -> ofDeprecatedMethodsRequestFactory().build(settings))
|
||||
.withMessageContaining("setReadTimeout method marked as deprecated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithSupplierWhenWrappedRequestFactoryTypeWithConnectTimeout() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withConnectTimeout(Duration.ofMillis(1234));
|
||||
SimpleClientHttpRequestFactory wrappedRequestFactory = new SimpleClientHttpRequestFactory();
|
||||
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
|
||||
.of(() -> new BufferingClientHttpRequestFactory(wrappedRequestFactory))
|
||||
.build(settings);
|
||||
assertThat(requestFactory).extracting("requestFactory").isSameAs(wrappedRequestFactory);
|
||||
assertThat(wrappedRequestFactory).hasFieldOrPropertyWithValue("connectTimeout", 1234);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithSupplierWhenWrappedRequestFactoryTypeWithReadTimeout() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withReadTimeout(Duration.ofMillis(1234));
|
||||
SimpleClientHttpRequestFactory wrappedRequestFactory = new SimpleClientHttpRequestFactory();
|
||||
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
|
||||
.of(() -> new BufferingClientHttpRequestFactory(wrappedRequestFactory))
|
||||
.build(settings);
|
||||
assertThat(requestFactory).extracting("requestFactory").isSameAs(wrappedRequestFactory);
|
||||
assertThat(wrappedRequestFactory).hasFieldOrPropertyWithValue("readTimeout", 1234);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithClassWhenHasMultipleTimeoutSettersFavorsDurationMethods() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(1))
|
||||
.withReadTimeout(Duration.ofSeconds(2));
|
||||
IntAndDurationTimeoutsClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
|
||||
.of(IntAndDurationTimeoutsClientHttpRequestFactory.class)
|
||||
.build(settings);
|
||||
assertThat((requestFactory).connectTimeout).isZero();
|
||||
assertThat((requestFactory).readTimeout).isZero();
|
||||
assertThat((requestFactory).connectTimeoutDuration).isEqualTo(Duration.ofSeconds(1));
|
||||
assertThat((requestFactory).readTimeoutDuration).isEqualTo(Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactoryBuilder<TestClientHttpRequestFactory> ofTestRequestFactory() {
|
||||
return ClientHttpRequestFactoryBuilder.of(TestClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactoryBuilder<UnconfigurableClientHttpRequestFactory> ofUnconfigurableRequestFactory() {
|
||||
return ClientHttpRequestFactoryBuilder.of(UnconfigurableClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
private ClientHttpRequestFactoryBuilder<DeprecatedMethodsClientHttpRequestFactory> ofDeprecatedMethodsRequestFactory() {
|
||||
return ClientHttpRequestFactoryBuilder.of(DeprecatedMethodsClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(ClientHttpRequestFactory requestFactory) {
|
||||
return ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).getConnectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(ClientHttpRequestFactory requestFactory) {
|
||||
return (long) ReflectionTestUtils.getField(requestFactory, "readTimeout");
|
||||
}
|
||||
|
||||
public static class TestClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
private int connectTimeout;
|
||||
|
||||
private int readTimeout;
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void setConnectTimeout(int timeout) {
|
||||
this.connectTimeout = timeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(int timeout) {
|
||||
this.readTimeout = timeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class UnconfigurableClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class DeprecatedMethodsClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.0.0", forRemoval = false)
|
||||
public void setConnectTimeout(int timeout) {
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.0.0", forRemoval = false)
|
||||
public void setReadTimeout(int timeout) {
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.0.0", forRemoval = false)
|
||||
public void setBufferRequestBody(boolean bufferRequestBody) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class IntAndDurationTimeoutsClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
|
||||
private int readTimeout;
|
||||
|
||||
private int connectTimeout;
|
||||
|
||||
private Duration readTimeoutDuration;
|
||||
|
||||
private Duration connectTimeoutDuration;
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void setConnectTimeout(int timeout) {
|
||||
this.connectTimeout = timeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(int timeout) {
|
||||
this.readTimeout = timeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Duration timeout) {
|
||||
this.connectTimeoutDuration = timeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(Duration timeout) {
|
||||
this.readTimeoutDuration = timeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleClientHttpRequestFactoryBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class SimpleClientHttpRequestFactoryBuilderTests
|
||||
extends AbstractClientHttpRequestFactoryBuilderTests<SimpleClientHttpRequestFactory> {
|
||||
|
||||
SimpleClientHttpRequestFactoryBuilderTests() {
|
||||
super(SimpleClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.simple());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(SimpleClientHttpRequestFactory requestFactory) {
|
||||
return (int) ReflectionTestUtils.getField(requestFactory, "connectTimeout");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(SimpleClientHttpRequestFactory requestFactory) {
|
||||
return (int) ReflectionTestUtils.getField(requestFactory, "readTimeout");
|
||||
}
|
||||
|
||||
@Override
|
||||
void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception {
|
||||
assertThatIllegalStateException().isThrownBy(() -> super.connectWithSslBundleAndOptionsMismatch(httpMethod))
|
||||
.withMessage("SSL Options cannot be specified with Java connections");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "DELETE" })
|
||||
@Override
|
||||
void redirectDefault(String httpMethod) throws Exception {
|
||||
super.redirectDefault(httpMethod);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "DELETE" })
|
||||
@Override
|
||||
void redirectFollow(String httpMethod) throws Exception {
|
||||
super.redirectFollow(httpMethod);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "DELETE" })
|
||||
@Override
|
||||
void redirectDontFollow(String httpMethod) throws Exception {
|
||||
super.redirectDontFollow(httpMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) {
|
||||
return (httpMethod != HttpMethod.GET) ? HttpStatus.FOUND : HttpStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test customizer that can assert that it has been called.
|
||||
*
|
||||
* @param <T> type being customized
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TestCustomizer<T> implements Consumer<T> {
|
||||
|
||||
private boolean called;
|
||||
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
this.called = true;
|
||||
}
|
||||
|
||||
void assertCalled() {
|
||||
assertThat(this.called).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.tomcat.servlet.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.HttpStatus;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunctions;
|
||||
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Base class for {@link ClientHttpConnectorBuilder} tests.
|
||||
*
|
||||
* @param <T> The {@link ClientHttpConnector} type
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
abstract class AbstractClientHttpConnectorBuilderTests<T extends ClientHttpConnector> {
|
||||
|
||||
private static final Function<HttpMethod, HttpStatus> ALWAYS_FOUND = (method) -> HttpStatus.FOUND;
|
||||
|
||||
private final Class<T> connectorType;
|
||||
|
||||
private final ClientHttpConnectorBuilder<T> builder;
|
||||
|
||||
AbstractClientHttpConnectorBuilderTests(Class<T> connectorType, ClientHttpConnectorBuilder<T> builder) {
|
||||
this.connectorType = connectorType;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReturnsConnectorOfExpectedType() {
|
||||
T connector = this.builder.build();
|
||||
assertThat(connector).isInstanceOf(this.connectorType);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHasConnectTimeout() {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(60));
|
||||
T connector = this.builder.build(settings);
|
||||
assertThat(connectTimeout(connector)).isEqualTo(Duration.ofSeconds(60).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHadReadTimeout() {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withReadTimeout(Duration.ofSeconds(120));
|
||||
T connector = this.builder.build(settings);
|
||||
assertThat(readTimeout(connector)).isEqualTo(Duration.ofSeconds(120).toMillis());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@WithPackageResources("test.jks")
|
||||
@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));
|
||||
ClientHttpConnector insecureConnector = this.builder.build();
|
||||
ClientRequest insecureRequest = createRequest(httpMethod, uri);
|
||||
assertThatExceptionOfType(WebClientRequestException.class)
|
||||
.isThrownBy(() -> getResponse(insecureConnector, insecureRequest))
|
||||
.withCauseInstanceOf(SSLHandshakeException.class);
|
||||
ClientHttpConnector secureConnector = this.builder
|
||||
.build(ClientHttpConnectorSettings.ofSslBundle(sslBundle()));
|
||||
ClientRequest secureRequest = createRequest(httpMethod, uri);
|
||||
ClientResponse secureResponse = getResponse(secureConnector, secureRequest);
|
||||
assertThat(secureResponse.bodyToMono(String.class).block())
|
||||
.contains("Received " + httpMethod + " request to /");
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@WithPackageResources("test.jks")
|
||||
@ValueSource(strings = { "GET", "POST" })
|
||||
void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
webServerFactory.setSsl(ssl("TLS_AES_128_GCM_SHA256"));
|
||||
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));
|
||||
ClientHttpConnector secureConnector = this.builder.build(ClientHttpConnectorSettings
|
||||
.ofSslBundle(sslBundle(SslOptions.of(Set.of("TLS_AES_256_GCM_SHA384"), null))));
|
||||
ClientRequest secureRequest = createRequest(httpMethod, uri);
|
||||
assertThatExceptionOfType(WebClientRequestException.class)
|
||||
.isThrownBy(() -> getResponse(secureConnector, secureRequest))
|
||||
.withCauseInstanceOf(SSLHandshakeException.class);
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectDefault(String httpMethod) throws Exception {
|
||||
testRedirect(null, HttpMethod.valueOf(httpMethod), this::getExpectedRedirect);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectFollow(String httpMethod) throws Exception {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withRedirects(HttpRedirects.FOLLOW);
|
||||
testRedirect(settings, HttpMethod.valueOf(httpMethod), this::getExpectedRedirect);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectDontFollow(String httpMethod) throws Exception {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
testRedirect(settings, HttpMethod.valueOf(httpMethod), ALWAYS_FOUND);
|
||||
}
|
||||
|
||||
protected final void testRedirect(ClientHttpConnectorSettings settings, HttpMethod httpMethod,
|
||||
Function<HttpMethod, HttpStatus> expectedStatusForMethod) throws URISyntaxException {
|
||||
HttpStatus expectedStatus = expectedStatusForMethod.apply(httpMethod);
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect");
|
||||
ClientHttpConnector connector = this.builder.build(settings);
|
||||
ClientRequest request = createRequest(httpMethod, uri);
|
||||
ClientResponse response = getResponse(connector, request);
|
||||
assertThat(response.statusCode()).isEqualTo(expectedStatus);
|
||||
if (expectedStatus == HttpStatus.OK) {
|
||||
assertThat(response.bodyToMono(String.class).block()).contains("request to /redirected");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private ClientRequest createRequest(String httpMethod, URI uri) {
|
||||
return createRequest(HttpMethod.valueOf(httpMethod), uri);
|
||||
}
|
||||
|
||||
private ClientRequest createRequest(HttpMethod httpMethod, URI uri) {
|
||||
return ClientRequest.create(httpMethod, uri).build();
|
||||
}
|
||||
|
||||
private ClientResponse getResponse(ClientHttpConnector connector, ClientRequest request) {
|
||||
return ExchangeFunctions.create(connector).exchange(request).block();
|
||||
}
|
||||
|
||||
private Ssl ssl(String... ciphers) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setClientAuth(ClientAuth.NEED);
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setTrustStore("classpath:test.jks");
|
||||
if (ciphers.length > 0) {
|
||||
ssl.setCiphers(ciphers);
|
||||
}
|
||||
return ssl;
|
||||
}
|
||||
|
||||
protected final SslBundle sslBundle() {
|
||||
return sslBundle(SslOptions.NONE);
|
||||
}
|
||||
|
||||
protected final SslBundle sslBundle(SslOptions sslOptions) {
|
||||
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks");
|
||||
JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails);
|
||||
return SslBundle.of(stores, SslBundleKey.of("password"), sslOptions);
|
||||
}
|
||||
|
||||
protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) {
|
||||
return HttpStatus.OK;
|
||||
}
|
||||
|
||||
protected abstract long connectTimeout(T connector);
|
||||
|
||||
protected abstract long readTimeout(T connector);
|
||||
|
||||
public static class TestServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
|
||||
if ("/redirect".equals(req.getRequestURI())) {
|
||||
res.sendRedirect("/redirected");
|
||||
return;
|
||||
}
|
||||
res.getWriter().println("Received " + req.getMethod() + " request to " + req.getRequestURI());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.HttpRoute;
|
||||
import org.apache.hc.client5.http.async.HttpAsyncClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.function.Resolver;
|
||||
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpConnectorBuilder} and
|
||||
* {@link HttpComponentsHttpAsyncClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpComponentsClientHttpConnectorBuilderTests
|
||||
extends AbstractClientHttpConnectorBuilderTests<HttpComponentsClientHttpConnector> {
|
||||
|
||||
HttpComponentsClientHttpConnectorBuilderTests() {
|
||||
super(HttpComponentsClientHttpConnector.class, ClientHttpConnectorBuilder.httpComponents());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpAsyncClientBuilder> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpAsyncClientBuilder> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<ConnectionConfig.Builder> connectionConfigCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<ConnectionConfig.Builder> connectionConfigCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<RequestConfig.Builder> defaultRequestConfigCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<RequestConfig.Builder> defaultRequestConfigCustomizer1 = new TestCustomizer<>();
|
||||
ClientHttpConnectorBuilder.httpComponents()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.withConnectionManagerCustomizer(connectionManagerCustomizer)
|
||||
.withConnectionConfigCustomizer(connectionConfigCustomizer1)
|
||||
.withConnectionConfigCustomizer(connectionConfigCustomizer2)
|
||||
.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer)
|
||||
.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer1)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
connectionManagerCustomizer.assertCalled();
|
||||
connectionConfigCustomizer1.assertCalled();
|
||||
connectionConfigCustomizer2.assertCalled();
|
||||
defaultRequestConfigCustomizer.assertCalled();
|
||||
defaultRequestConfigCustomizer1.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void withTlsSocketStrategyFactory() {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.ofSslBundle(sslBundle());
|
||||
List<SslBundle> bundles = new ArrayList<>();
|
||||
Function<SslBundle, TlsStrategy> tlsSocketStrategyFactory = (bundle) -> {
|
||||
bundles.add(bundle);
|
||||
return (sessionLayer, host, localAddress, remoteAddress, attachment, handshakeTimeout) -> false;
|
||||
};
|
||||
ClientHttpConnectorBuilder.httpComponents()
|
||||
.withTlsSocketStrategyFactory(tlsSocketStrategyFactory)
|
||||
.build(settings);
|
||||
assertThat(bundles).contains(settings.sslBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(HttpComponentsClientHttpConnector connector) {
|
||||
return getConnectorConfig(connector).getConnectTimeout().toMilliseconds();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(HttpComponentsClientHttpConnector connector) {
|
||||
return getConnectorConfig(connector).getSocketTimeout().toMilliseconds();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ConnectionConfig getConnectorConfig(HttpComponentsClientHttpConnector connector) {
|
||||
HttpAsyncClient httpClient = (HttpAsyncClient) ReflectionTestUtils.getField(connector, "client");
|
||||
Object manager = ReflectionTestUtils.getField(httpClient, "manager");
|
||||
ConnectionConfig connectorConfig = ((Resolver<HttpRoute, ConnectionConfig>) ReflectionTestUtils
|
||||
.getField(manager, "connectionConfigResolver")).resolve(null);
|
||||
return connectorConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.JdkHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpConnectorBuilder} and {@link JdkHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBuilderTests<JdkClientHttpConnector> {
|
||||
|
||||
JdkClientHttpConnectorBuilderTests() {
|
||||
super(JdkClientHttpConnector.class, ClientHttpConnectorBuilder.jdk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClient.Builder> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClient.Builder> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JdkClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
return httpClient.connectTimeout().get().toMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(JdkClientHttpConnector connector) {
|
||||
Duration readTimeout = (Duration) ReflectionTestUtils.getField(connector, "readTimeout");
|
||||
return readTimeout.toMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.JettyHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyClientHttpConnectorBuilder} and {@link JettyHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JettyClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBuilderTests<JettyClientHttpConnector> {
|
||||
|
||||
JettyClientHttpConnectorBuilderTests() {
|
||||
super(JettyClientHttpConnector.class, ClientHttpConnectorBuilder.jetty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClient> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClient> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClientTransport> httpClientTransportCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<ClientConnector> clientConnectorCustomizerCustomizer = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.jetty()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.withHttpClientTransportCustomizer(httpClientTransportCustomizer)
|
||||
.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
httpClientTransportCustomizer.assertCalled();
|
||||
clientConnectorCustomizerCustomizer.assertCalled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JettyClientHttpConnector connector) {
|
||||
return ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).getConnectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(JettyClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
return ((Duration) ReflectionTestUtils.getField(httpClient, "readTimeout")).toMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorClientHttpConnectorBuilder} and
|
||||
* {@link ReactorHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ReactorClientHttpConnectorBuilderTests
|
||||
extends AbstractClientHttpConnectorBuilderTests<ReactorClientHttpConnector> {
|
||||
|
||||
ReactorClientHttpConnectorBuilderTests() {
|
||||
super(ReactorClientHttpConnector.class, ClientHttpConnectorBuilder.reactor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHttpClientFactory() {
|
||||
boolean[] called = new boolean[1];
|
||||
Supplier<HttpClient> httpClientFactory = () -> {
|
||||
called[0] = true;
|
||||
return HttpClient.create();
|
||||
};
|
||||
ClientHttpConnectorBuilder.reactor().withHttpClientFactory(httpClientFactory).build();
|
||||
assertThat(called).containsExactly(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withReactorResourceFactory() {
|
||||
ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory());
|
||||
ClientHttpConnectorBuilder.reactor().withReactorResourceFactory(resourceFactory).build();
|
||||
then(resourceFactory).should().getConnectionProvider();
|
||||
then(resourceFactory).should().getLoopResources();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
List<HttpClient> httpClients = new ArrayList<>();
|
||||
UnaryOperator<HttpClient> httpClientCustomizer1 = (httpClient) -> {
|
||||
httpClients.add(httpClient);
|
||||
return httpClient;
|
||||
};
|
||||
UnaryOperator<HttpClient> httpClientCustomizer2 = (httpClient) -> {
|
||||
httpClients.add(httpClient);
|
||||
return httpClient;
|
||||
};
|
||||
ClientHttpConnectorBuilder.reactor()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.build();
|
||||
assertThat(httpClients).hasSize(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(ReactorClientHttpConnector connector) {
|
||||
return (int) ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).configuration()
|
||||
.options()
|
||||
.get(ChannelOption.CONNECT_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(ReactorClientHttpConnector connector) {
|
||||
return (int) ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).configuration()
|
||||
.responseTimeout()
|
||||
.toMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test customizer that can assert that it has been called.
|
||||
*
|
||||
* @param <T> type being customized
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TestCustomizer<T> implements Consumer<T> {
|
||||
|
||||
private boolean called;
|
||||
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
this.called = true;
|
||||
}
|
||||
|
||||
void assertCalled() {
|
||||
assertThat(this.called).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.boot.http.client.reactive.JdkClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.JettyClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpConnectorAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpConnectorAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(ClientHttpConnectorAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void whenReactorIsAvailableThenReactorBeansAreDefined() {
|
||||
this.contextRunner.run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context).hasSingleBean(ReactorResourceFactory.class);
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorIsUnavailableThenJettyClientBeansAreDefined() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class)).run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorAndHttpClientAreUnavailableThenJettyClientBeansAreDefined() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class, HttpAsyncClients.class))
|
||||
.run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorAndHttpClientAndJettyAreUnavailableThenJdkClientBeansAreDefined() {
|
||||
this.contextRunner
|
||||
.withClassLoader(new FilteredClassLoader(HttpClient.class, HttpAsyncClients.class,
|
||||
org.eclipse.jetty.client.HttpClient.class))
|
||||
.run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOverrideCustomClientConnector() {
|
||||
this.contextRunner.withUserConfiguration(CustomClientHttpConnectorConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClientHttpConnector.class);
|
||||
assertThat(context).hasBean("customConnector");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomReactorResourceFactory() {
|
||||
this.contextRunner.withUserConfiguration(CustomReactorResourceConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClientHttpConnector.class);
|
||||
assertThat(context).hasSingleBean(ReactorResourceFactory.class);
|
||||
assertThat(context).hasBean("customReactorResourceFactory");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresDetectedClientHttpConnectorBuilderBuilder() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientHttpConnectorBuilder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresDefinedClientHttpConnectorBuilder() {
|
||||
this.contextRunner.withPropertyValues("spring.http.reactiveclient.connector=jetty")
|
||||
.run((context) -> assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isInstanceOf(JettyClientHttpConnectorBuilder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresClientHttpConnectorSettings() {
|
||||
this.contextRunner.withPropertyValues(sslPropertyValues().toArray(String[]::new))
|
||||
.withPropertyValues("spring.http.reactiveclient.redirects=dont-follow",
|
||||
"spring.http.reactiveclient.connect-timeout=10s", "spring.http.reactiveclient.read-timeout=20s",
|
||||
"spring.http.reactiveclient.ssl.bundle=test")
|
||||
.run((context) -> {
|
||||
ClientHttpConnectorSettings settings = context.getBean(ClientHttpConnectorSettings.class);
|
||||
assertThat(settings.redirects()).isEqualTo(HttpRedirects.DONT_FOLLOW);
|
||||
assertThat(settings.connectTimeout()).isEqualTo(Duration.ofSeconds(10));
|
||||
assertThat(settings.readTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(settings.sslBundle().getKey().getAlias()).isEqualTo("alias1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeConditionalOnAtLeastOneHttpConnectorClass() {
|
||||
FilteredClassLoader classLoader = new FilteredClassLoader(reactor.netty.http.client.HttpClient.class,
|
||||
org.eclipse.jetty.client.HttpClient.class, org.apache.hc.client5.http.impl.async.HttpAsyncClients.class,
|
||||
java.net.http.HttpClient.class);
|
||||
assertThatIllegalStateException().as("enough filtering")
|
||||
.isThrownBy(() -> ClientHttpConnectorBuilder.detect(classLoader));
|
||||
this.contextRunner.withClassLoader(classLoader)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ClientHttpConnectorSettings.class));
|
||||
}
|
||||
|
||||
private List<String> sslPropertyValues() {
|
||||
List<String> propertyValues = new ArrayList<>();
|
||||
String location = "classpath:org/springframework/boot/autoconfigure/ssl/";
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.key.alias=alias1");
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.truststore.type=PKCS12");
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.truststore.certificate=" + location + "rsa-cert.pem");
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.truststore.private-key=" + location + "rsa-key.pem");
|
||||
return propertyValues;
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientHttpConnectorBuilderCustomizersAreApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.http.reactiveclient.connector=jdk")
|
||||
.withUserConfiguration(ClientHttpConnectorBuilderCustomizersConfiguration.class)
|
||||
.run((context) -> {
|
||||
ClientHttpConnector connector = context.getBean(ClientHttpConnectorBuilder.class).build();
|
||||
assertThat(connector).extracting("readTimeout").isEqualTo(Duration.ofSeconds(5));
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomClientHttpConnectorConfig {
|
||||
|
||||
@Bean
|
||||
ClientHttpConnector customConnector() {
|
||||
return mock(ClientHttpConnector.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomReactorResourceConfig {
|
||||
|
||||
@Bean
|
||||
ReactorResourceFactory customReactorResourceFactory() {
|
||||
return new ReactorResourceFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ClientHttpConnectorBuilderCustomizersConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpConnectorBuilderCustomizer<JdkClientHttpConnectorBuilder> jdkCustomizer() {
|
||||
return (builder) -> builder.withCustomizer((connector) -> connector.setReadTimeout(Duration.ofSeconds(5)));
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientHttpConnectorBuilderCustomizer<JettyClientHttpConnectorBuilder> jettyCustomizer() {
|
||||
return (builder) -> {
|
||||
throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.boot.http.client.reactive.JettyClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.autoconfigure.AbstractClientHttpConnectorProperties.Connector;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpConnectors}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpConnectorsTests {
|
||||
|
||||
private final DefaultSslBundleRegistry bundleRegistry = new DefaultSslBundleRegistry();
|
||||
|
||||
private ObjectFactory<SslBundles> sslBundles = () -> this.bundleRegistry;
|
||||
|
||||
@Test
|
||||
void builderWhenHasConnectorPropertyReturnsFirst() {
|
||||
TestProperties p1 = new TestProperties();
|
||||
TestProperties p2 = new TestProperties();
|
||||
p2.setConnector(Connector.JETTY);
|
||||
TestProperties p3 = new TestProperties();
|
||||
p3.setConnector(Connector.JDK);
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, p1, p2, p3);
|
||||
assertThat(connectors.builder(null)).isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHasNoConnectorPropertyReturnsDetected() {
|
||||
TestProperties properties = new TestProperties();
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, properties);
|
||||
assertThat(connectors.builder(null)).isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsWhenHasNoSettingProperties() {
|
||||
TestProperties properties = new TestProperties();
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, properties);
|
||||
ClientHttpConnectorSettings settings = connectors.settings();
|
||||
assertThat(settings).isEqualTo(new ClientHttpConnectorSettings(null, null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsWhenHasMultipleSettingProperties() {
|
||||
this.bundleRegistry.registerBundle("p2", mock(SslBundle.class));
|
||||
this.bundleRegistry.registerBundle("p3", mock(SslBundle.class));
|
||||
TestProperties p1 = new TestProperties();
|
||||
TestProperties p2 = new TestProperties();
|
||||
p2.setRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
p2.setConnectTimeout(Duration.ofSeconds(1));
|
||||
p2.setReadTimeout(Duration.ofSeconds(2));
|
||||
p2.getSsl().setBundle("p2");
|
||||
TestProperties p3 = new TestProperties();
|
||||
p3.setRedirects(HttpRedirects.FOLLOW);
|
||||
p3.setConnectTimeout(Duration.ofSeconds(10));
|
||||
p3.setReadTimeout(Duration.ofSeconds(20));
|
||||
p3.getSsl().setBundle("p3");
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, p1, p2, p3);
|
||||
ClientHttpConnectorSettings settings = connectors.settings();
|
||||
assertThat(settings).isEqualTo(new ClientHttpConnectorSettings(HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1),
|
||||
Duration.ofSeconds(2), this.bundleRegistry.getBundle("p2")));
|
||||
}
|
||||
|
||||
static class TestProperties extends AbstractClientHttpConnectorProperties {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.reactive.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.reactive.HttpComponentsClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.JdkClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.JettyClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.autoconfigure.AbstractClientHttpConnectorProperties.Connector;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpReactiveClientProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpReactiveClientSettingsPropertiesTests {
|
||||
|
||||
@Nested
|
||||
class ConnectorTests {
|
||||
|
||||
@Test
|
||||
void reactorBuilder() {
|
||||
assertThat(Connector.REACTOR.builder()).isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyBuilder() {
|
||||
assertThat(Connector.JETTY.builder()).isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpComponentsBuilder() {
|
||||
assertThat(Connector.HTTP_COMPONENTS.builder())
|
||||
.isInstanceOf(HttpComponentsClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdkBuilder() {
|
||||
assertThat(Connector.JDK.builder()).isInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.actuate.observation;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ObservationRestClientCustomizer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ObservationRestClientCustomizerTests {
|
||||
|
||||
private static final String TEST_METRIC_NAME = "http.test.metric.name";
|
||||
|
||||
private final ObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||
|
||||
private final RestClient.Builder restClientBuilder = RestClient.builder();
|
||||
|
||||
private final ObservationRestClientCustomizer customizer = new ObservationRestClientCustomizer(
|
||||
this.observationRegistry, new DefaultClientRequestObservationConvention(TEST_METRIC_NAME));
|
||||
|
||||
@Test
|
||||
void shouldCustomizeObservationConfiguration() {
|
||||
this.customizer.customize(this.restClientBuilder);
|
||||
assertThat(this.restClientBuilder).hasFieldOrPropertyWithValue("observationRegistry", this.observationRegistry);
|
||||
assertThat(this.restClientBuilder).extracting("observationConvention")
|
||||
.isInstanceOf(DefaultClientRequestObservationConvention.class)
|
||||
.hasFieldOrPropertyWithValue("name", TEST_METRIC_NAME);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.actuate.observation;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ObservationRestTemplateCustomizer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ObservationRestTemplateCustomizerTests {
|
||||
|
||||
private static final String TEST_METRIC_NAME = "http.test.metric.name";
|
||||
|
||||
private final ObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
private final ObservationRestTemplateCustomizer customizer = new ObservationRestTemplateCustomizer(
|
||||
this.observationRegistry, new DefaultClientRequestObservationConvention(TEST_METRIC_NAME));
|
||||
|
||||
@Test
|
||||
void shouldCustomizeObservationConfiguration() {
|
||||
this.customizer.customize(this.restTemplate);
|
||||
assertThat(this.restTemplate).hasFieldOrPropertyWithValue("observationRegistry", this.observationRegistry);
|
||||
assertThat(this.restTemplate).extracting("observationConvention")
|
||||
.isInstanceOf(DefaultClientRequestObservationConvention.class)
|
||||
.hasFieldOrPropertyWithValue("name", TEST_METRIC_NAME);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AutoConfiguredRestClientSsl}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AutoConfiguredRestClientSslTests {
|
||||
|
||||
private final ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings
|
||||
.ofSslBundle(mock(SslBundle.class, "Default SslBundle"))
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW)
|
||||
.withReadTimeout(Duration.ofSeconds(10))
|
||||
.withConnectTimeout(Duration.ofSeconds(30));
|
||||
|
||||
@Mock
|
||||
private SslBundles sslBundles;
|
||||
|
||||
@Mock
|
||||
private ClientHttpRequestFactoryBuilder<ClientHttpRequestFactory> factoryBuilder;
|
||||
|
||||
@Mock
|
||||
private ClientHttpRequestFactory factory;
|
||||
|
||||
private AutoConfiguredRestClientSsl restClientSsl;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
this.restClientSsl = new AutoConfiguredRestClientSsl(this.factoryBuilder, this.settings, this.sslBundles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureRestClientUsingBundleName() {
|
||||
String bundleName = "test";
|
||||
SslBundle sslBundle = mock(SslBundle.class, "SslBundle named '%s'".formatted(bundleName));
|
||||
given(this.sslBundles.getBundle(bundleName)).willReturn(sslBundle);
|
||||
given(this.factoryBuilder.build(this.settings.withSslBundle(sslBundle))).willReturn(this.factory);
|
||||
RestClient restClient = build(this.restClientSsl.fromBundle(bundleName));
|
||||
assertThat(restClient).hasFieldOrPropertyWithValue("clientRequestFactory", this.factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureRestClientUsingBundle() {
|
||||
SslBundle sslBundle = mock(SslBundle.class, "Custom SslBundle");
|
||||
given(this.factoryBuilder.build(this.settings.withSslBundle(sslBundle))).willReturn(this.factory);
|
||||
RestClient restClient = build(this.restClientSsl.fromBundle(sslBundle));
|
||||
assertThat(restClient).hasFieldOrPropertyWithValue("clientRequestFactory", this.factory);
|
||||
}
|
||||
|
||||
private RestClient build(Consumer<RestClient.Builder> customizer) {
|
||||
Builder builder = RestClient.builder();
|
||||
customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpMessageConvertersRestClientCustomizer}
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpMessageConvertersRestClientCustomizerTests {
|
||||
|
||||
@Test
|
||||
void createWhenNullMessageConvertersArrayThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new HttpMessageConvertersRestClientCustomizer((HttpMessageConverter<?>[]) null))
|
||||
.withMessage("'messageConverters' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenNullMessageConvertersDoesNotCustomize() {
|
||||
HttpMessageConverter<?> c0 = mock();
|
||||
assertThat(apply(new HttpMessageConvertersRestClientCustomizer((HttpMessageConverters) null), c0))
|
||||
.containsExactly(c0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeConfiguresMessageConverters() {
|
||||
HttpMessageConverter<?> c0 = mock();
|
||||
HttpMessageConverter<?> c1 = mock();
|
||||
HttpMessageConverter<?> c2 = mock();
|
||||
assertThat(apply(new HttpMessageConvertersRestClientCustomizer(c1, c2), c0)).containsExactly(c1, c2);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<HttpMessageConverter<?>> apply(HttpMessageConvertersRestClientCustomizer customizer,
|
||||
HttpMessageConverter<?>... converters) {
|
||||
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(Arrays.asList(converters));
|
||||
RestClient.Builder restClientBuilder = mock();
|
||||
ArgumentCaptor<Consumer<List<HttpMessageConverter<?>>>> captor = ArgumentCaptor.forClass(Consumer.class);
|
||||
given(restClientBuilder.messageConverters(captor.capture())).willReturn(restClientBuilder);
|
||||
customizer.customize(restClientBuilder);
|
||||
captor.getValue().accept(messageConverters);
|
||||
return messageConverters;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RestClientAutoConfiguration}
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Moritz Halbritter
|
||||
* @author Dmytro Nosan
|
||||
* @author Dmitry Sulman
|
||||
*/
|
||||
class RestClientAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, HttpClientAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void shouldSupplyBeans() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
|
||||
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
|
||||
assertThat(context).hasSingleBean(RestClient.Builder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupplyRestClientSslIfSslBundlesIsThereWithCustomHttpSettingsAndBuilder() {
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
ClientHttpRequestFactorySettings clientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW)
|
||||
.withConnectTimeout(Duration.ofHours(1))
|
||||
.withReadTimeout(Duration.ofDays(1))
|
||||
.withSslBundle(mock(SslBundle.class));
|
||||
ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder = mock(
|
||||
ClientHttpRequestFactoryBuilder.class);
|
||||
this.contextRunner.withBean(SslBundles.class, () -> sslBundles)
|
||||
.withBean(ClientHttpRequestFactorySettings.class, () -> clientHttpRequestFactorySettings)
|
||||
.withBean(ClientHttpRequestFactoryBuilder.class, () -> clientHttpRequestFactoryBuilder)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestClientSsl.class);
|
||||
RestClientSsl restClientSsl = context.getBean(RestClientSsl.class);
|
||||
assertThat(restClientSsl).hasFieldOrPropertyWithValue("sslBundles", sslBundles);
|
||||
assertThat(restClientSsl).hasFieldOrPropertyWithValue("builder", clientHttpRequestFactoryBuilder);
|
||||
assertThat(restClientSsl).hasFieldOrPropertyWithValue("settings", clientHttpRequestFactorySettings);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupplyRestClientSslIfSslBundlesIsThereWithAutoConfiguredHttpSettingsAndBuilder() {
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
this.contextRunner.withBean(SslBundles.class, () -> sslBundles).run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestClientSsl.class)
|
||||
.hasSingleBean(ClientHttpRequestFactorySettings.class)
|
||||
.hasSingleBean(ClientHttpRequestFactoryBuilder.class);
|
||||
RestClientSsl restClientSsl = context.getBean(RestClientSsl.class);
|
||||
assertThat(restClientSsl).hasFieldOrPropertyWithValue("sslBundles", sslBundles);
|
||||
assertThat(restClientSsl).hasFieldOrPropertyWithValue("builder",
|
||||
context.getBean(ClientHttpRequestFactoryBuilder.class));
|
||||
assertThat(restClientSsl).hasFieldOrPropertyWithValue("settings",
|
||||
context.getBean(ClientHttpRequestFactorySettings.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateBuilder() {
|
||||
this.contextRunner.run((context) -> {
|
||||
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
|
||||
RestClient restClient = builder.build();
|
||||
assertThat(restClient).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurerShouldCallCustomizers() {
|
||||
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
|
||||
RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class);
|
||||
RestClientCustomizer customizer = context.getBean("restClientCustomizer", RestClientCustomizer.class);
|
||||
Builder builder = RestClient.builder();
|
||||
configurer.configure(builder);
|
||||
then(customizer).should().customize(builder);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void restClientShouldApplyCustomizers() {
|
||||
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
|
||||
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
|
||||
RestClientCustomizer customizer = context.getBean("restClientCustomizer", RestClientCustomizer.class);
|
||||
builder.build();
|
||||
then(customizer).should().customize(any(RestClient.Builder.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetPrototypeScopedBean() {
|
||||
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
|
||||
RestClient.Builder firstBuilder = context.getBean(RestClient.Builder.class);
|
||||
RestClient.Builder secondBuilder = context.getBean(RestClient.Builder.class);
|
||||
assertThat(firstBuilder).isNotEqualTo(secondBuilder);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCreateClientBuilderIfAlreadyPresent() {
|
||||
this.contextRunner.withUserConfiguration(CustomRestClientBuilderConfig.class).run((context) -> {
|
||||
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
|
||||
assertThat(builder).isInstanceOf(MyRestClientBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void restClientWhenMessageConvertersDefinedShouldHaveMessageConverters() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(RestClientConfig.class)
|
||||
.run((context) -> {
|
||||
RestClient restClient = context.getBean(RestClient.class);
|
||||
List<HttpMessageConverter<?>> expectedConverters = context.getBean(HttpMessageConverters.class)
|
||||
.getConverters();
|
||||
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
|
||||
.getField(restClient, "messageConverters");
|
||||
assertThat(actualConverters).containsExactlyElementsOf(expectedConverters);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void restClientWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
|
||||
this.contextRunner.withUserConfiguration(RestClientConfig.class).run((context) -> {
|
||||
RestClient restClient = context.getBean(RestClient.class);
|
||||
RestClient defaultRestClient = RestClient.builder().build();
|
||||
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
|
||||
.getField(restClient, "messageConverters");
|
||||
List<HttpMessageConverter<?>> expectedConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
|
||||
.getField(defaultRestClient, "messageConverters");
|
||||
assertThat(actualConverters).hasSameSizeAs(expectedConverters);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
void restClientWhenHasCustomMessageConvertersShouldHaveMessageConverters() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(CustomHttpMessageConverter.class, RestClientConfig.class)
|
||||
.run((context) -> {
|
||||
RestClient restClient = context.getBean(RestClient.class);
|
||||
List<HttpMessageConverter<?>> actualConverters = (List<HttpMessageConverter<?>>) ReflectionTestUtils
|
||||
.getField(restClient, "messageConverters");
|
||||
assertThat(actualConverters).extracting(HttpMessageConverter::getClass)
|
||||
.contains((Class) CustomHttpMessageConverter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasFactoryProperty() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(RestClientConfig.class)
|
||||
.withPropertyValues("spring.http.client.factory=simple")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestClient.class);
|
||||
RestClient restClient = context.getBean(RestClient.class);
|
||||
assertThat(restClient).extracting("clientRequestFactory")
|
||||
.isInstanceOf(SimpleClientHttpRequestFactory.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupplyRestClientBuilderConfigurerWithCustomSettings() {
|
||||
ClientHttpRequestFactorySettings clientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder = mock(
|
||||
ClientHttpRequestFactoryBuilder.class);
|
||||
RestClientCustomizer customizer1 = mock(RestClientCustomizer.class);
|
||||
RestClientCustomizer customizer2 = mock(RestClientCustomizer.class);
|
||||
HttpMessageConvertersRestClientCustomizer httpMessageConverterCustomizer = mock(
|
||||
HttpMessageConvertersRestClientCustomizer.class);
|
||||
this.contextRunner.withBean(ClientHttpRequestFactorySettings.class, () -> clientHttpRequestFactorySettings)
|
||||
.withBean(ClientHttpRequestFactoryBuilder.class, () -> clientHttpRequestFactoryBuilder)
|
||||
.withBean("customizer1", RestClientCustomizer.class, () -> customizer1)
|
||||
.withBean("customizer2", RestClientCustomizer.class, () -> customizer2)
|
||||
.withBean("httpMessageConverterCustomizer", HttpMessageConvertersRestClientCustomizer.class,
|
||||
() -> httpMessageConverterCustomizer)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class)
|
||||
.hasSingleBean(ClientHttpRequestFactorySettings.class)
|
||||
.hasSingleBean(ClientHttpRequestFactoryBuilder.class);
|
||||
RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class);
|
||||
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactoryBuilder",
|
||||
clientHttpRequestFactoryBuilder);
|
||||
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactorySettings",
|
||||
clientHttpRequestFactorySettings);
|
||||
assertThat(configurer).hasFieldOrPropertyWithValue("customizers",
|
||||
List.of(customizer1, customizer2, httpMessageConverterCustomizer));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupplyRestClientBuilderConfigurerWithAutoConfiguredHttpSettings() {
|
||||
RestClientCustomizer customizer1 = mock(RestClientCustomizer.class);
|
||||
RestClientCustomizer customizer2 = mock(RestClientCustomizer.class);
|
||||
this.contextRunner.withBean("customizer1", RestClientCustomizer.class, () -> customizer1)
|
||||
.withBean("customizer2", RestClientCustomizer.class, () -> customizer2)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class)
|
||||
.hasSingleBean(ClientHttpRequestFactorySettings.class)
|
||||
.hasSingleBean(ClientHttpRequestFactoryBuilder.class)
|
||||
.hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
|
||||
RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class);
|
||||
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactoryBuilder",
|
||||
context.getBean(ClientHttpRequestFactoryBuilder.class));
|
||||
assertThat(configurer).hasFieldOrPropertyWithValue("requestFactorySettings",
|
||||
context.getBean(ClientHttpRequestFactorySettings.class));
|
||||
assertThat(configurer).hasFieldOrPropertyWithValue("customizers", List.of(customizer1, customizer2,
|
||||
context.getBean(HttpMessageConvertersRestClientCustomizer.class)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactiveWebApplicationRestClientIsNotConfigured() {
|
||||
new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class))
|
||||
.run((context) -> {
|
||||
assertThat(context).doesNotHaveBean(HttpMessageConvertersRestClientCustomizer.class);
|
||||
assertThat(context).doesNotHaveBean(RestClientBuilderConfigurer.class);
|
||||
assertThat(context).doesNotHaveBean(RestClient.Builder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServletWebApplicationRestClientIsConfigured() {
|
||||
new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class))
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
|
||||
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
|
||||
assertThat(context).hasSingleBean(RestClient.Builder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_21)
|
||||
void whenReactiveWebApplicationAndVirtualThreadsEnabledAndTaskExecutorBean() {
|
||||
new ReactiveWebApplicationContextRunner().withPropertyValues("spring.threads.virtual.enabled=true")
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(RestClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class))
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class);
|
||||
assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class);
|
||||
assertThat(context).hasSingleBean(RestClient.Builder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_21)
|
||||
void whenReactiveWebApplicationAndVirtualThreadsDisabled() {
|
||||
new ReactiveWebApplicationContextRunner().withPropertyValues("spring.threads.virtual.enabled=false")
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(RestClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(RestClient.Builder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_21)
|
||||
void whenReactiveWebApplicationAndVirtualThreadsEnabledAndNoTaskExecutorBean() {
|
||||
new ReactiveWebApplicationContextRunner().withPropertyValues("spring.threads.virtual.enabled=true")
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(RestClient.Builder.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestClientCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
RestClientCustomizer restClientCustomizer() {
|
||||
return mock(RestClientCustomizer.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomRestClientBuilderConfig {
|
||||
|
||||
@Bean
|
||||
MyRestClientBuilder myRestClientBuilder() {
|
||||
return mock(MyRestClientBuilder.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface MyRestClientBuilder extends RestClient.Builder {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
RestClient restClient(RestClient.Builder restClientBuilder) {
|
||||
return restClientBuilder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomHttpMessageConverter extends StringHttpMessageConverter {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RestClientBuilderConfigurer}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RestClientBuilderConfigurerTests {
|
||||
|
||||
@Mock
|
||||
private ClientHttpRequestFactoryBuilder<ClientHttpRequestFactory> clientHttpRequestFactoryBuilder;
|
||||
|
||||
@Mock
|
||||
private ClientHttpRequestFactory clientHttpRequestFactory;
|
||||
|
||||
@Test
|
||||
void shouldConfigureRestClientBuilder() {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.ofSslBundle(mock(SslBundle.class));
|
||||
RestClientCustomizer customizer = mock(RestClientCustomizer.class);
|
||||
RestClientCustomizer customizer1 = mock(RestClientCustomizer.class);
|
||||
RestClientBuilderConfigurer configurer = new RestClientBuilderConfigurer(this.clientHttpRequestFactoryBuilder,
|
||||
settings, List.of(customizer, customizer1));
|
||||
given(this.clientHttpRequestFactoryBuilder.build(settings)).willReturn(this.clientHttpRequestFactory);
|
||||
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
configurer.configure(builder);
|
||||
assertThat(builder.build()).hasFieldOrPropertyWithValue("clientRequestFactory", this.clientHttpRequestFactory);
|
||||
then(customizer).should().customize(builder);
|
||||
then(customizer1).should().customize(builder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.rest.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
||||
import org.springframework.boot.web.client.RestTemplateRequestCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RestTemplateAutoConfiguration}
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class RestTemplateAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(RestTemplateAutoConfiguration.class, HttpClientAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void restTemplateBuilderConfigurerShouldBeLazilyDefined() {
|
||||
this.contextRunner.run((context) -> assertThat(
|
||||
context.getBeanFactory().getBeanDefinition("restTemplateBuilderConfigurer").isLazyInit())
|
||||
.isTrue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailOnCustomRestTemplateBuilderConfigurer() {
|
||||
this.contextRunner.withUserConfiguration(RestTemplateBuilderConfigurerConfig.class)
|
||||
.run((context) -> assertThat(context).getFailure()
|
||||
.isInstanceOf(BeanDefinitionOverrideException.class)
|
||||
.hasMessageContaining("with name 'restTemplateBuilderConfigurer'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateBuilderShouldBeLazilyDefined() {
|
||||
this.contextRunner
|
||||
.run((context) -> assertThat(context.getBeanFactory().getBeanDefinition("restTemplateBuilder").isLazyInit())
|
||||
.isTrue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateWhenMessageConvertersDefinedShouldHaveMessageConverters() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(RestTemplateConfig.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
List<HttpMessageConverter<?>> converters = context.getBean(HttpMessageConverters.class).getConverters();
|
||||
assertThat(restTemplate.getMessageConverters()).containsExactlyElementsOf(converters);
|
||||
assertThat(restTemplate.getRequestFactory()).isInstanceOf(HttpComponentsClientHttpRequestFactory.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() {
|
||||
this.contextRunner.withUserConfiguration(RestTemplateConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
assertThat(restTemplate.getMessageConverters()).hasSameSizeAs(new RestTemplate().getMessageConverters());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
void restTemplateWhenHasCustomMessageConvertersShouldHaveMessageConverters() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(CustomHttpMessageConverter.class, RestTemplateConfig.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
assertThat(restTemplate.getMessageConverters()).extracting(HttpMessageConverter::getClass)
|
||||
.contains((Class) CustomHttpMessageConverter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateShouldApplyCustomizer() {
|
||||
this.contextRunner.withUserConfiguration(RestTemplateConfig.class, RestTemplateCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
RestTemplateCustomizer customizer = context.getBean(RestTemplateCustomizer.class);
|
||||
then(customizer).should().customize(restTemplate);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateWhenHasCustomBuilderShouldUseCustomBuilder() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(RestTemplateConfig.class, CustomRestTemplateBuilderConfig.class,
|
||||
RestTemplateCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
assertThat(restTemplate.getMessageConverters()).hasSize(1);
|
||||
assertThat(restTemplate.getMessageConverters().get(0)).isInstanceOf(CustomHttpMessageConverter.class);
|
||||
then(context.getBean(RestTemplateCustomizer.class)).shouldHaveNoInteractions();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateWhenHasCustomBuilderCouldReuseBuilderConfigurer() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(RestTemplateConfig.class, CustomRestTemplateBuilderWithConfigurerConfig.class,
|
||||
RestTemplateCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
assertThat(restTemplate.getMessageConverters()).hasSize(1);
|
||||
assertThat(restTemplate.getMessageConverters().get(0)).isInstanceOf(CustomHttpMessageConverter.class);
|
||||
RestTemplateCustomizer customizer = context.getBean(RestTemplateCustomizer.class);
|
||||
then(customizer).should().customize(restTemplate);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void restTemplateShouldApplyRequestCustomizer() {
|
||||
this.contextRunner.withUserConfiguration(RestTemplateRequestCustomizerConfig.class).run((context) -> {
|
||||
RestTemplateBuilder builder = context.getBean(RestTemplateBuilder.class);
|
||||
ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class);
|
||||
MockClientHttpRequest request = new MockClientHttpRequest();
|
||||
request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK));
|
||||
given(requestFactory.createRequest(any(), any())).willReturn(request);
|
||||
RestTemplate restTemplate = builder.requestFactory(() -> requestFactory).build();
|
||||
restTemplate.getForEntity("http://localhost:8080/test", String.class);
|
||||
assertThat(request.getHeaders().headerSet()).contains(entry("spring", Collections.singletonList("boot")));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void builderShouldBeFreshForEachUse() {
|
||||
this.contextRunner.withUserConfiguration(DirtyRestTemplateConfig.class)
|
||||
.run((context) -> assertThat(context).hasNotFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServletWebApplicationRestTemplateBuilderIsConfigured() {
|
||||
new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class))
|
||||
.run((context) -> assertThat(context).hasSingleBean(RestTemplateBuilder.class)
|
||||
.hasSingleBean(RestTemplateBuilderConfigurer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactiveWebApplicationRestTemplateBuilderIsNotConfigured() {
|
||||
new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(RestTemplateBuilder.class)
|
||||
.doesNotHaveBean(RestTemplateBuilderConfigurer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasFactoryProperty() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(RestTemplateConfig.class)
|
||||
.withPropertyValues("spring.http.client.factory=simple")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
RestTemplate restTemplate = context.getBean(RestTemplate.class);
|
||||
assertThat(restTemplate.getRequestFactory()).isInstanceOf(SimpleClientHttpRequestFactory.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DirtyRestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplate restTemplateOne(RestTemplateBuilder builder) {
|
||||
try {
|
||||
return builder.build();
|
||||
}
|
||||
finally {
|
||||
breakBuilderOnNextCall(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestTemplate restTemplateTwo(RestTemplateBuilder builder) {
|
||||
try {
|
||||
return builder.build();
|
||||
}
|
||||
finally {
|
||||
breakBuilderOnNextCall(builder);
|
||||
}
|
||||
}
|
||||
|
||||
private void breakBuilderOnNextCall(RestTemplateBuilder builder) {
|
||||
builder.additionalCustomizers((restTemplate) -> {
|
||||
throw new IllegalStateException();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomRestTemplateBuilderConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplateBuilder restTemplateBuilder() {
|
||||
return new RestTemplateBuilder().messageConverters(new CustomHttpMessageConverter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomRestTemplateBuilderWithConfigurerConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) {
|
||||
return configurer.configure(new RestTemplateBuilder()).messageConverters(new CustomHttpMessageConverter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestTemplateCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplateCustomizer restTemplateCustomizer() {
|
||||
return mock(RestTemplateCustomizer.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestTemplateRequestCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplateRequestCustomizer<?> restTemplateRequestCustomizer() {
|
||||
return (request) -> request.getHeaders().add("spring", "boot");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestTemplateBuilderConfigurerConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplateBuilderConfigurer restTemplateBuilderConfigurer() {
|
||||
return new RestTemplateBuilderConfigurer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomHttpMessageConverter extends StringHttpMessageConverter {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.autoconfigure.AbstractHttpRequestFactoryProperties.Factory;
|
||||
import org.springframework.boot.http.client.service.autoconfigure.HttpClientServiceProperties.Group;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpClientServiceProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpClientServicePropertiesTests {
|
||||
|
||||
@Test
|
||||
void bindProperties() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.setProperty("spring.http.client.service.base-url", "https://example.com");
|
||||
environment.setProperty("spring.http.client.service.default-header.secure", "very,somewhat");
|
||||
environment.setProperty("spring.http.client.service.default-header.test", "true");
|
||||
environment.setProperty("spring.http.client.service.factory", "jetty");
|
||||
environment.setProperty("spring.http.client.service.redirects", "dont-follow");
|
||||
environment.setProperty("spring.http.client.service.connect-timeout", "1s");
|
||||
environment.setProperty("spring.http.client.service.read-timeout", "2s");
|
||||
environment.setProperty("spring.http.client.service.ssl.bundle", "usual");
|
||||
environment.setProperty("spring.http.client.service.group.olga.base-url", "https://example.com/olga");
|
||||
environment.setProperty("spring.http.client.service.group.olga.default-header.secure", "nope");
|
||||
environment.setProperty("spring.http.client.service.group.olga.factory", "reactor");
|
||||
environment.setProperty("spring.http.client.service.group.olga.redirects", "follow");
|
||||
environment.setProperty("spring.http.client.service.group.olga.connect-timeout", "10s");
|
||||
environment.setProperty("spring.http.client.service.group.olga.read-timeout", "20s");
|
||||
environment.setProperty("spring.http.client.service.group.olga.ssl.bundle", "unusual");
|
||||
environment.setProperty("spring.http.client.service.group.rossen.base-url", "https://example.com/rossen");
|
||||
environment.setProperty("spring.http.client.service.group.phil.base-url", "https://example.com/phil");
|
||||
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
|
||||
applicationContext.setEnvironment(environment);
|
||||
applicationContext.register(PropertiesConfiguration.class);
|
||||
applicationContext.refresh();
|
||||
HttpClientServiceProperties properties = applicationContext.getBean(HttpClientServiceProperties.class);
|
||||
assertThat(properties.getBaseUrl()).isEqualTo("https://example.com");
|
||||
assertThat(properties.getDefaultHeader()).containsOnly(Map.entry("secure", List.of("very", "somewhat")),
|
||||
Map.entry("test", List.of("true")));
|
||||
assertThat(properties.getFactory()).isEqualTo(Factory.JETTY);
|
||||
assertThat(properties.getRedirects()).isEqualTo(HttpRedirects.DONT_FOLLOW);
|
||||
assertThat(properties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1));
|
||||
assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(2));
|
||||
assertThat(properties.getSsl().getBundle()).isEqualTo("usual");
|
||||
assertThat(properties.getGroup()).containsOnlyKeys("olga", "rossen", "phil");
|
||||
assertThat(properties.getGroup().get("olga").getBaseUrl()).isEqualTo("https://example.com/olga");
|
||||
assertThat(properties.getGroup().get("rossen").getBaseUrl()).isEqualTo("https://example.com/rossen");
|
||||
assertThat(properties.getGroup().get("phil").getBaseUrl()).isEqualTo("https://example.com/phil");
|
||||
Group groupProperties = properties.getGroup().get("olga");
|
||||
assertThat(groupProperties.getDefaultHeader()).containsOnly(Map.entry("secure", List.of("nope")));
|
||||
assertThat(groupProperties.getFactory()).isEqualTo(Factory.REACTOR);
|
||||
assertThat(groupProperties.getRedirects()).isEqualTo(HttpRedirects.FOLLOW);
|
||||
assertThat(groupProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(10));
|
||||
assertThat(groupProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(groupProperties.getSsl().getBundle()).isEqualTo("unusual");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(HttpClientServiceProperties.class)
|
||||
static class PropertiesConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.http.client.service.autoconfigure;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.extractor.Extractors;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.http.client.rest.autoconfigure.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceClientAutoConfiguration},
|
||||
* {@link RestClientPropertiesHttpServiceGroupConfigurer} and
|
||||
* {@link RestClientCustomizerHttpServiceGroupConfigurer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpServiceClientAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(HttpServiceClientAutoConfiguration.class,
|
||||
HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void configuresClientFromProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.client.service.base-url=https://example.com",
|
||||
"spring.http.client.service.default-header.test=true",
|
||||
"spring.http.client.service.group.one.base-url=https://example.com/one",
|
||||
"spring.http.client.service.group.two.default-header.two=iam2")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class)
|
||||
.run((context) -> {
|
||||
HttpServiceProxyRegistry serviceProxyRegistry = context.getBean(HttpServiceProxyRegistry.class);
|
||||
assertThat(serviceProxyRegistry.getGroupNames()).containsOnly("one", "two");
|
||||
MockRestServiceServerConfiguration mockServers = context
|
||||
.getBean(MockRestServiceServerConfiguration.class);
|
||||
MockRestServiceServer serverOne = mockServers.getMock("one");
|
||||
serverOne.expect(requestTo("https://example.com/one/hello"))
|
||||
.andExpect(header("test", "true"))
|
||||
.andRespond(withSuccess().body("world!"));
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(clientOne.hello()).isEqualTo("world!");
|
||||
MockRestServiceServer serverTwo = mockServers.getMock("two");
|
||||
serverTwo.expect((request) -> request.getURI().toString().equals("https://example.com/"))
|
||||
.andExpect(header("test", "true"))
|
||||
.andExpect(header("two", "iam2"))
|
||||
.andRespond(withSuccess().body("boot!"));
|
||||
TestClientTwo clientTwo = context.getBean(TestClientTwo.class);
|
||||
assertThat(clientTwo.there()).isEqualTo("boot!");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedRequestFactoryBuilder() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, RequestFactoryBuilderConfiguration.class)
|
||||
.run((context) -> {
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedRequestFactorySettings() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.client.service.base-url=https://example.com",
|
||||
"spring.http.client.factory=jdk")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, RequestFactorySettingsConfiguration.class)
|
||||
.run((context) -> {
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedRestClientCustomizer() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class,
|
||||
RestClientCustomizerConfiguration.class)
|
||||
.run((context) -> {
|
||||
MockRestServiceServerConfiguration mockServers = context
|
||||
.getBean(MockRestServiceServerConfiguration.class);
|
||||
MockRestServiceServer serverOne = mockServers.getMock("one");
|
||||
serverOne.expect(requestTo("https://example.com/hello"))
|
||||
.andExpect(header("customized", "true"))
|
||||
.andRespond(withSuccess().body("world!"));
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(clientOne.hello()).isEqualTo("world!");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedHttpServiceGroupConfigurer() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class,
|
||||
HttpServiceGroupConfigurerConfiguration.class)
|
||||
.run((context) -> {
|
||||
MockRestServiceServerConfiguration mockServers = context
|
||||
.getBean(MockRestServiceServerConfiguration.class);
|
||||
MockRestServiceServer serverOne = mockServers.getMock("one");
|
||||
serverOne.expect(requestTo("https://example.com/hello"))
|
||||
.andExpect(header("customizedgroup", "true"))
|
||||
.andRespond(withSuccess().body("world!"));
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(clientOne.hello()).isEqualTo("world!");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNoHttpServiceProxyRegistryBean() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class));
|
||||
}
|
||||
|
||||
private HttpClient getJdkHttpClient(Object proxy) {
|
||||
return (HttpClient) Extractors.byName("clientRequestFactory.httpClient").apply(getRestClient(proxy));
|
||||
}
|
||||
|
||||
private RestClient getRestClient(Object proxy) {
|
||||
InvocationHandler handler = Proxy.getInvocationHandler(proxy);
|
||||
Advisor[] advisors = (Advisor[]) Extractors.byName("advised.advisors").apply(handler);
|
||||
Map<?, ?> serviceMethods = (Map<?, ?>) Extractors.byName("advice.httpServiceMethods").apply(advisors[0]);
|
||||
Object serviceMethod = serviceMethods.values().iterator().next();
|
||||
return (RestClient) Extractors.byName("responseFunction.responseFunction.arg$1.restClient")
|
||||
.apply(serviceMethod);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MockRestServiceServerConfiguration {
|
||||
|
||||
private Map<String, MockRestServiceServer> mocks = new HashMap<>();
|
||||
|
||||
@Bean
|
||||
RestClientHttpServiceGroupConfigurer mockServerConfigurer() {
|
||||
return (groups) -> groups.forEachClient(this::addMock);
|
||||
}
|
||||
|
||||
private MockRestServiceServer addMock(HttpServiceGroup group, Builder client) {
|
||||
return this.mocks.put(group.name(), MockRestServiceServer.bindTo(client).build());
|
||||
}
|
||||
|
||||
MockRestServiceServer getMock(String name) {
|
||||
return this.mocks.get(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportHttpServices(group = "one", types = TestClientOne.class)
|
||||
@ImportHttpServices(group = "two", types = TestClientTwo.class)
|
||||
static class HttpClientConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RequestFactoryBuilderConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder() {
|
||||
return ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withHttpClientCustomizer((httpClient) -> httpClient.followRedirects(Redirect.NEVER));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RequestFactorySettingsConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpRequestFactorySettings requestFactorySettings() {
|
||||
return ClientHttpRequestFactorySettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestClientCustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
RestClientCustomizer restClientCustomizer() {
|
||||
return (builder) -> builder.defaultHeader("customized", "true");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HttpServiceGroupConfigurerConfiguration {
|
||||
|
||||
@Bean
|
||||
RestClientHttpServiceGroupConfigurer restClientHttpServiceGroupConfigurer() {
|
||||
return (groups) -> groups.filterByName("one")
|
||||
.forEachClient((group, builder) -> builder.defaultHeader("customizedgroup", "true"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface TestClientOne {
|
||||
|
||||
@GetExchange("/hello")
|
||||
String hello();
|
||||
|
||||
}
|
||||
|
||||
interface TestClientTwo {
|
||||
|
||||
@GetExchange("/there")
|
||||
String there();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user