mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-02 23:39:34 +00:00
Polish 'Support virtual threading with JDK HTTP clients'
See gh-46404
This commit is contained in:
+13
-7
@@ -19,7 +19,7 @@ package org.springframework.boot.http.client;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -35,6 +35,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @author Sangmin Park
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public final class JdkClientHttpRequestFactoryBuilder
|
||||
@@ -63,6 +64,17 @@ public final class JdkClientHttpRequestFactoryBuilder
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpRequestFactoryBuilder} uses the given executor
|
||||
* with the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param executor the executor to use
|
||||
* @return a new {@link JdkClientHttpRequestFactoryBuilder} instance
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public JdkClientHttpRequestFactoryBuilder withExecutor(Executor executor) {
|
||||
return new JdkClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientBuilder.withExecutor(executor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
@@ -76,12 +88,6 @@ public final class JdkClientHttpRequestFactoryBuilder
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
public JdkClientHttpRequestFactoryBuilder enableVirtualThreadExecutor() {
|
||||
return this.withHttpClientCustomizer(builder ->
|
||||
builder.executor(Executors.newVirtualThreadPerTaskExecutor())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withReadTimeout(null)));
|
||||
|
||||
+13
@@ -18,6 +18,7 @@ package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLParameters;
|
||||
@@ -49,6 +50,18 @@ public final class JdkHttpClientBuilder {
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkHttpClientBuilder} uses the given executor with the
|
||||
* underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param executor the executor to use
|
||||
* @return a new {@link JdkHttpClientBuilder} instance
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public JdkHttpClientBuilder withExecutor(Executor executor) {
|
||||
Assert.notNull(executor, "'executor' must not be null");
|
||||
return withCustomizer((httpClient) -> httpClient.executor(executor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkHttpClientBuilder} that applies additional customization to
|
||||
* the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
|
||||
+12
-16
@@ -24,17 +24,18 @@ 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.condition.ConditionalOnThreading;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.thread.Threading;
|
||||
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.JdkClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.thread.Threading;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,7 @@ import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
* {@link ClientHttpRequestFactoryBuilder} and {@link ClientHttpRequestFactorySettings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Sangmin Park
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@@ -53,10 +55,14 @@ public final class HttpClientAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
private final ClientHttpRequestFactories factories;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
HttpClientAutoConfiguration(ObjectProvider<SslBundles> sslBundles, HttpClientProperties properties) {
|
||||
HttpClientAutoConfiguration(Environment environment, ObjectProvider<SslBundles> sslBundles,
|
||||
HttpClientProperties properties) {
|
||||
this.environment = environment;
|
||||
this.factories = new ClientHttpRequestFactories(sslBundles, properties);
|
||||
}
|
||||
|
||||
@@ -67,21 +73,11 @@ public final class HttpClientAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnThreading(Threading.PLATFORM)
|
||||
ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilderOnPlatform(
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilderCustomizer<?>> clientHttpRequestFactoryBuilderCustomizers) {
|
||||
ClientHttpRequestFactoryBuilder<?> builder = this.factories.builder(this.beanClassLoader);
|
||||
return customize(builder, clientHttpRequestFactoryBuilderCustomizers.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnThreading(Threading.VIRTUAL)
|
||||
ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilderOnVirtual(
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilderCustomizer<?>> clientHttpRequestFactoryBuilderCustomizers) {
|
||||
ClientHttpRequestFactoryBuilder<?> builder = this.factories.builder(this.beanClassLoader);
|
||||
if (builder instanceof JdkClientHttpRequestFactoryBuilder jdk) {
|
||||
return customize(jdk.enableVirtualThreadExecutor(), clientHttpRequestFactoryBuilderCustomizers.orderedStream().toList());
|
||||
if (builder instanceof JdkClientHttpRequestFactoryBuilder jdk && Threading.VIRTUAL.isActive(this.environment)) {
|
||||
builder = jdk.withExecutor(new VirtualThreadTaskExecutor("httpclient-"));
|
||||
}
|
||||
return customize(builder, clientHttpRequestFactoryBuilderCustomizers.orderedStream().toList());
|
||||
}
|
||||
@@ -91,7 +87,7 @@ public final class HttpClientAutoConfiguration implements BeanClassLoaderAware {
|
||||
List<ClientHttpRequestFactoryBuilderCustomizer<?>> customizers) {
|
||||
ClientHttpRequestFactoryBuilder<?>[] builderReference = { builder };
|
||||
LambdaSafe.callbacks(ClientHttpRequestFactoryBuilderCustomizer.class, customizers, builderReference[0])
|
||||
.invoke((customizer) -> builderReference[0] = customizer.customize(builderReference[0]));
|
||||
.invoke((customizer) -> builderReference[0] = customizer.customize(builderReference[0]));
|
||||
return builderReference[0];
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -30,9 +30,11 @@ 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.http.client.reactive.JdkClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.reactor.netty.autoconfigure.ReactorNettyConfigurations;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.thread.Threading;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
@@ -40,6 +42,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
@@ -58,11 +62,14 @@ public final class ClientHttpConnectorAutoConfiguration implements BeanClassLoad
|
||||
|
||||
private final ClientHttpConnectors connectors;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
ClientHttpConnectorAutoConfiguration(ObjectProvider<SslBundles> sslBundles,
|
||||
ClientHttpConnectorAutoConfiguration(Environment environment, ObjectProvider<SslBundles> sslBundles,
|
||||
HttpReactiveClientProperties properties) {
|
||||
this.environment = environment;
|
||||
this.connectors = new ClientHttpConnectors(sslBundles, properties);
|
||||
}
|
||||
|
||||
@@ -76,6 +83,9 @@ public final class ClientHttpConnectorAutoConfiguration implements BeanClassLoad
|
||||
ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder(
|
||||
ObjectProvider<ClientHttpConnectorBuilderCustomizer<?>> clientHttpConnectorBuilderCustomizers) {
|
||||
ClientHttpConnectorBuilder<?> builder = this.connectors.builder(this.beanClassLoader);
|
||||
if (builder instanceof JdkClientHttpConnectorBuilder jdk && Threading.VIRTUAL.isActive(this.environment)) {
|
||||
builder = jdk.withExecutor(new VirtualThreadTaskExecutor("httpclient-"));
|
||||
}
|
||||
return customize(builder, clientHttpConnectorBuilderCustomizers.orderedStream().toList());
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -19,6 +19,7 @@ package org.springframework.boot.http.client.reactive;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -59,6 +60,17 @@ public final class JdkClientHttpConnectorBuilder extends AbstractClientHttpConne
|
||||
return new JdkClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpConnectorBuilder} uses the given executor with the
|
||||
* underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param executor the executor to use
|
||||
* @return a new {@link JdkClientHttpConnectorBuilder} instance
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public JdkClientHttpConnectorBuilder withExecutor(Executor executor) {
|
||||
return new JdkClientHttpConnectorBuilder(getCustomizers(), this.httpClientBuilder.withExecutor(executor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
|
||||
+12
@@ -18,12 +18,16 @@ package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpRequestFactoryBuilder} and {@link JdkHttpClientBuilder}.
|
||||
*
|
||||
@@ -48,6 +52,14 @@ class JdkClientHttpRequestFactoryBuilderTests
|
||||
httpClientCustomizer2.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withExecutor() {
|
||||
Executor executor = new SimpleAsyncTaskExecutor();
|
||||
JdkClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jdk().withExecutor(executor).build();
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient");
|
||||
assertThat(httpClient.executor()).containsSame(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JdkClientHttpRequestFactory requestFactory) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient");
|
||||
|
||||
+16
@@ -16,11 +16,14 @@
|
||||
|
||||
package org.springframework.boot.http.client.autoconfigure;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
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.ssl.SslAutoConfiguration;
|
||||
@@ -37,7 +40,9 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -146,6 +151,17 @@ class HttpClientAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_21)
|
||||
void whenVirtualThreadsEnabledAndUsingJdkHttpClientUsesVirtualThreadExecutor() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.factory=jdk", "spring.threads.virtual.enabled=true")
|
||||
.run((context) -> {
|
||||
ClientHttpRequestFactory factory = context.getBean(ClientHttpRequestFactoryBuilder.class).build();
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient");
|
||||
assertThat(httpClient.executor().get()).isInstanceOf(VirtualThreadTaskExecutor.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ClientHttpRequestFactoryBuilderCustomizersConfiguration {
|
||||
|
||||
|
||||
+17
@@ -22,6 +22,8 @@ import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.resources.LoopResources;
|
||||
|
||||
@@ -38,8 +40,10 @@ 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.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
@@ -171,6 +175,19 @@ class ClientHttpConnectorAutoConfigurationTests {
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ClientHttpConnectorSettings.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForJreRange(min = JRE.JAVA_21)
|
||||
void whenVirtualThreadsEnabledAndUsingJdkHttpClientUsesVirtualThreadExecutor() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.reactiveclient.connector=jdk", "spring.threads.virtual.enabled=true")
|
||||
.run((context) -> {
|
||||
ClientHttpConnector connector = context.getBean(ClientHttpConnectorBuilder.class).build();
|
||||
java.net.http.HttpClient httpClient = (java.net.http.HttpClient) ReflectionTestUtils.getField(connector,
|
||||
"httpClient");
|
||||
assertThat(httpClient.executor().get()).isInstanceOf(VirtualThreadTaskExecutor.class);
|
||||
});
|
||||
}
|
||||
|
||||
private List<String> sslPropertyValues() {
|
||||
List<String> propertyValues = new ArrayList<>();
|
||||
String location = "classpath:org/springframework/boot/autoconfigure/ssl/";
|
||||
|
||||
+12
@@ -18,13 +18,17 @@ package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.JdkHttpClientBuilder;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpConnectorBuilder} and {@link JdkHttpClientBuilder}.
|
||||
*
|
||||
@@ -48,6 +52,14 @@ class JdkClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBuil
|
||||
httpClientCustomizer2.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withExecutor() {
|
||||
Executor executor = new SimpleAsyncTaskExecutor();
|
||||
JdkClientHttpConnector connector = ClientHttpConnectorBuilder.jdk().withExecutor(executor).build();
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
assertThat(httpClient.executor()).containsSame(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JdkClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
|
||||
Reference in New Issue
Block a user