Add ThreadPoolTaskExecutorBuilder and deprecate TaskExecutorBuilder

Closes gh-36637
This commit is contained in:
Moritz Halbritter
2023-07-31 09:21:43 +02:00
parent 5c845c7ddc
commit 32c91af440
10 changed files with 741 additions and 42 deletions

View File

@@ -16,18 +16,11 @@
package org.springframework.boot.autoconfigure.task;
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.task.TaskExecutionProperties.Shutdown;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -36,12 +29,15 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
*
* @author Stephane Nicoll
* @author Camille Vienot
* @author Moritz Halbritter
* @since 2.1.0
*/
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@AutoConfiguration
@EnableConfigurationProperties(TaskExecutionProperties.class)
@Import({ TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class,
@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
TaskExecutorConfigurations.TaskExecutorBuilderConfiguration.class,
TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class,
TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class })
public class TaskExecutionAutoConfiguration {
@@ -50,25 +46,4 @@ public class TaskExecutionAutoConfiguration {
*/
public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";
@Bean
@ConditionalOnMissingBean
public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
ObjectProvider<TaskDecorator> taskDecorator) {
TaskExecutionProperties.Pool pool = properties.getPool();
TaskExecutorBuilder builder = new TaskExecutorBuilder();
builder = builder.queueCapacity(pool.getQueueCapacity());
builder = builder.corePoolSize(pool.getCoreSize());
builder = builder.maxPoolSize(pool.getMaxSize());
builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
builder = builder.keepAlive(pool.getKeepAlive());
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(taskDecorator.getIfUnique());
return builder;
}
}

View File

@@ -21,8 +21,12 @@ import java.util.concurrent.Executor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
import org.springframework.boot.task.ThreadPoolTaskExecutorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@@ -37,6 +41,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
* {@link TaskExecutionAutoConfiguration} in a specific order.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
class TaskExecutorConfigurations {
@@ -59,13 +64,82 @@ class TaskExecutorConfigurations {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(Executor.class)
@SuppressWarnings("removal")
static class ThreadPoolTaskExecutorConfiguration {
@Lazy
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder taskExecutorBuilder,
ObjectProvider<ThreadPoolTaskExecutorBuilder> threadPoolTaskExecutorBuilderProvider) {
ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder = threadPoolTaskExecutorBuilderProvider
.getIfUnique();
if (threadPoolTaskExecutorBuilder != null) {
return threadPoolTaskExecutorBuilder.build();
}
return taskExecutorBuilder.build();
}
}
@Configuration(proxyBeanMethods = false)
@SuppressWarnings("removal")
static class TaskExecutorBuilderConfiguration {
@Bean
@ConditionalOnMissingBean
@Deprecated(since = "3.2.0", forRemoval = true)
TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
ObjectProvider<TaskDecorator> taskDecorator) {
TaskExecutionProperties.Pool pool = properties.getPool();
TaskExecutorBuilder builder = new TaskExecutorBuilder();
builder = builder.queueCapacity(pool.getQueueCapacity());
builder = builder.corePoolSize(pool.getCoreSize());
builder = builder.maxPoolSize(pool.getMaxSize());
builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
builder = builder.keepAlive(pool.getKeepAlive());
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(taskDecorator.getIfUnique());
return builder;
}
}
@Configuration(proxyBeanMethods = false)
@SuppressWarnings("removal")
static class ThreadPoolTaskExecutorBuilderConfiguration {
@Bean
@ConditionalOnMissingBean({ TaskExecutorBuilder.class, ThreadPoolTaskExecutorBuilder.class })
ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder(TaskExecutionProperties properties,
ObjectProvider<ThreadPoolTaskExecutorCustomizer> threadPoolTaskExecutorCustomizers,
ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
ObjectProvider<TaskDecorator> taskDecorator) {
TaskExecutionProperties.Pool pool = properties.getPool();
ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder();
builder = builder.queueCapacity(pool.getQueueCapacity());
builder = builder.corePoolSize(pool.getCoreSize());
builder = builder.maxPoolSize(pool.getMaxSize());
builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
builder = builder.keepAlive(pool.getKeepAlive());
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(threadPoolTaskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(taskDecorator.getIfUnique());
// Apply the deprecated TaskExecutorCustomizers, too
builder = builder.additionalCustomizers(taskExecutorCustomizers.orderedStream().map(this::adapt).toList());
return builder;
}
private ThreadPoolTaskExecutorCustomizer adapt(TaskExecutorCustomizer customizer) {
return customizer::customize;
}
}

View File

@@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -33,6 +33,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
@@ -57,13 +58,33 @@ import static org.mockito.Mockito.mock;
*
* @author Stephane Nicoll
* @author Camille Vienot
* @author Moritz Halbritter
*/
@ExtendWith(OutputCaptureExtension.class)
@SuppressWarnings("removal")
class TaskExecutionAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class));
@Test
void shouldSupplyBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(TaskExecutorBuilder.class);
assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class);
assertThat(context).hasSingleBean(ThreadPoolTaskExecutor.class);
});
}
@Test
void shouldNotSupplyThreadPoolTaskExecutorBuilderIfCustomTaskExecutorBuilderIsPresent() {
this.contextRunner.withBean(TaskExecutorBuilder.class, TaskExecutorBuilder::new).run((context) -> {
assertThat(context).hasSingleBean(TaskExecutorBuilder.class);
assertThat(context).doesNotHaveBean(ThreadPoolTaskExecutorBuilder.class);
assertThat(context).hasSingleBean(ThreadPoolTaskExecutor.class);
});
}
@Test
void taskExecutorBuilderShouldApplyCustomSettings() {
this.contextRunner
@@ -85,6 +106,27 @@ class TaskExecutionAutoConfigurationTests {
}));
}
@Test
void threadPoolTaskExecutorBuilderShouldApplyCustomSettings() {
this.contextRunner
.withPropertyValues("spring.task.execution.pool.queue-capacity=10",
"spring.task.execution.pool.core-size=2", "spring.task.execution.pool.max-size=4",
"spring.task.execution.pool.allow-core-thread-timeout=true",
"spring.task.execution.pool.keep-alive=5s", "spring.task.execution.shutdown.await-termination=true",
"spring.task.execution.shutdown.await-termination-period=30s",
"spring.task.execution.thread-name-prefix=mytest-")
.run(assertThreadPoolTaskExecutor((taskExecutor) -> {
assertThat(taskExecutor).hasFieldOrPropertyWithValue("queueCapacity", 10);
assertThat(taskExecutor.getCorePoolSize()).isEqualTo(2);
assertThat(taskExecutor.getMaxPoolSize()).isEqualTo(4);
assertThat(taskExecutor).hasFieldOrPropertyWithValue("allowCoreThreadTimeOut", true);
assertThat(taskExecutor.getKeepAliveSeconds()).isEqualTo(5);
assertThat(taskExecutor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true);
assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationMillis", 30000L);
assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-");
}));
}
@Test
void taskExecutorBuilderWhenHasCustomBuilderShouldUseCustomBuilder() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorBuilderConfig.class).run((context) -> {
@@ -94,6 +136,15 @@ class TaskExecutionAutoConfigurationTests {
});
}
@Test
void threadPoolTaskExecutorBuilderWhenHasCustomBuilderShouldUseCustomBuilder() {
this.contextRunner.withUserConfiguration(CustomThreadPoolTaskExecutorBuilderConfig.class).run((context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class);
assertThat(context.getBean(ThreadPoolTaskExecutorBuilder.class))
.isSameAs(context.getBean(CustomThreadPoolTaskExecutorBuilderConfig.class).builder);
});
}
@Test
void taskExecutorBuilderShouldUseTaskDecorator() {
this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class).run((context) -> {
@@ -103,6 +154,15 @@ class TaskExecutionAutoConfigurationTests {
});
}
@Test
void threadPoolTaskExecutorBuilderShouldUseTaskDecorator() {
this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class).run((context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class);
ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutorBuilder.class).build();
assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class));
});
}
@Test
void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() {
this.contextRunner.run((context) -> {
@@ -116,7 +176,7 @@ class TaskExecutionAutoConfigurationTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_20)
@EnabledForJreRange(min = JRE.JAVA_21)
void whenVirtualThreadsAreEnabledThenSimpleAsyncTaskExecutorWithVirtualThreadsIsAutoConfigured() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> {
assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor");
@@ -128,7 +188,7 @@ class TaskExecutionAutoConfigurationTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_20)
@EnabledForJreRange(min = JRE.JAVA_21)
void whenTaskNamePrefixIsConfiguredThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() {
this.contextRunner
.withPropertyValues("spring.threads.virtual.enabled=true",
@@ -141,7 +201,7 @@ class TaskExecutionAutoConfigurationTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_20)
@EnabledForJreRange(min = JRE.JAVA_21)
void whenVirtualThreadsAreAvailableButNotEnabledThenThreadPoolTaskExecutorIsAutoConfigured() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor");
@@ -150,7 +210,7 @@ class TaskExecutionAutoConfigurationTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_20)
@EnabledForJreRange(min = JRE.JAVA_21)
void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true")
.withUserConfiguration(TaskDecoratorConfig.class)
@@ -169,7 +229,7 @@ class TaskExecutionAutoConfigurationTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_20)
@EnabledForJreRange(min = JRE.JAVA_21)
void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class)
.withPropertyValues("spring.threads.virtual.enabled=true")
@@ -188,6 +248,15 @@ class TaskExecutionAutoConfigurationTests {
});
}
@Test
void threadPoolTaskExecutorBuilderShouldApplyCustomizer() {
this.contextRunner.withUserConfiguration(TaskExecutorCustomizerConfig.class).run((context) -> {
TaskExecutorCustomizer customizer = context.getBean(TaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutorBuilder.class).build();
then(customizer).should().customize(executor);
});
}
@Test
void enableAsyncUsesAutoConfiguredOneByDefault() {
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=task-test-")
@@ -212,6 +281,25 @@ class TaskExecutionAutoConfigurationTests {
});
}
@Test
void customTaskExecutorBuilderOverridesThreadPoolTaskExecutorBuilder() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorBuilderConfig.class).run((context) -> {
ThreadPoolTaskExecutor bean = context.getBean(ThreadPoolTaskExecutor.class);
assertThat(bean.getThreadNamePrefix()).isEqualTo("CustomTaskExecutorBuilderConfig-");
});
}
@Test
void threadPoolTaskExecutorBuilderAppliesTaskExecutorCustomizer() {
this.contextRunner
.withBean(TaskExecutorCustomizer.class,
() -> (taskExecutor) -> taskExecutor.setThreadNamePrefix("custom-prefix-"))
.run((context) -> {
ThreadPoolTaskExecutor bean = context.getBean(ThreadPoolTaskExecutor.class);
assertThat(bean.getThreadNamePrefix()).isEqualTo("custom-prefix-");
});
}
private ContextConsumer<AssertableApplicationContext> assertTaskExecutor(
Consumer<ThreadPoolTaskExecutor> taskExecutor) {
return (context) -> {
@@ -221,6 +309,15 @@ class TaskExecutionAutoConfigurationTests {
};
}
private ContextConsumer<AssertableApplicationContext> assertThreadPoolTaskExecutor(
Consumer<ThreadPoolTaskExecutor> taskExecutor) {
return (context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class);
ThreadPoolTaskExecutorBuilder builder = context.getBean(ThreadPoolTaskExecutorBuilder.class);
taskExecutor.accept(builder.build());
};
}
private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException {
AtomicReference<Thread> threadReference = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
@@ -238,7 +335,8 @@ class TaskExecutionAutoConfigurationTests {
@Configuration(proxyBeanMethods = false)
static class CustomTaskExecutorBuilderConfig {
private final TaskExecutorBuilder taskExecutorBuilder = new TaskExecutorBuilder();
private final TaskExecutorBuilder taskExecutorBuilder = new TaskExecutorBuilder()
.threadNamePrefix("CustomTaskExecutorBuilderConfig-");
@Bean
TaskExecutorBuilder customTaskExecutorBuilder() {
@@ -247,6 +345,18 @@ class TaskExecutionAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomThreadPoolTaskExecutorBuilderConfig {
private final ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder();
@Bean
ThreadPoolTaskExecutorBuilder customThreadPoolTaskExecutorBuilder() {
return this.builder;
}
}
@Configuration(proxyBeanMethods = false)
static class TaskExecutorCustomizerConfig {

View File

@@ -16,7 +16,7 @@ If you have defined a custom `Executor` in the context, both regular task execut
However, the Spring MVC and Spring WebFlux support will only use it if it is an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`).
Depending on your target arrangement, you could change your `Executor` into an `AsyncTaskExecutor` or define both an `AsyncTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`.
The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default.
The auto-configured `ThreadPoolTaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default.
====
When a `ThreadPoolTaskExecutor` is auto-configured, the thread pool uses 8 core threads that can grow and shrink according to the load.
@@ -49,4 +49,4 @@ The thread pool uses one thread by default and its settings can be fine-tuned us
size: 2
----
Both a `TaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created.
Both a `ThreadPoolTaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 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.
@@ -42,7 +42,11 @@ import org.springframework.util.CollectionUtils;
* @author Stephane Nicoll
* @author Filip Hrisafov
* @since 2.1.0
* @deprecated since 3.2.0 for removal in 3.4.0 in favor of
* {@link ThreadPoolTaskExecutorBuilder}
*/
@Deprecated(since = "3.2.0", forRemoval = true)
@SuppressWarnings("removal")
public class TaskExecutorBuilder {
private final Integer queueCapacity;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2023 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.
@@ -24,8 +24,11 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
* @author Stephane Nicoll
* @since 2.1.0
* @see TaskExecutorBuilder
* @deprecated since 3.2.0 for removal in 3.4.0 in favor of
* {@link ThreadPoolTaskExecutorCustomizer}
*/
@FunctionalInterface
@Deprecated(since = "3.2.0", forRemoval = true)
public interface TaskExecutorCustomizer {
/**

View File

@@ -0,0 +1,326 @@
/*
* Copyright 2012-2023 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.task;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Builder that can be used to configure and create a {@link ThreadPoolTaskExecutor}.
* Provides convenience methods to set common {@link ThreadPoolTaskExecutor} settings and
* register {@link #taskDecorator(TaskDecorator)}). For advanced configuration, consider
* using {@link ThreadPoolTaskExecutorCustomizer}.
* <p>
* In a typical auto-configured Spring Boot application this builder is available as a
* bean and can be injected whenever a {@link ThreadPoolTaskExecutor} is needed.
*
* @author Stephane Nicoll
* @author Filip Hrisafov
* @since 3.2.0
*/
public class ThreadPoolTaskExecutorBuilder {
private final Integer queueCapacity;
private final Integer corePoolSize;
private final Integer maxPoolSize;
private final Boolean allowCoreThreadTimeOut;
private final Duration keepAlive;
private final Boolean awaitTermination;
private final Duration awaitTerminationPeriod;
private final String threadNamePrefix;
private final TaskDecorator taskDecorator;
private final Set<ThreadPoolTaskExecutorCustomizer> customizers;
public ThreadPoolTaskExecutorBuilder() {
this.queueCapacity = null;
this.corePoolSize = null;
this.maxPoolSize = null;
this.allowCoreThreadTimeOut = null;
this.keepAlive = null;
this.awaitTermination = null;
this.awaitTerminationPeriod = null;
this.threadNamePrefix = null;
this.taskDecorator = null;
this.customizers = null;
}
private ThreadPoolTaskExecutorBuilder(Integer queueCapacity, Integer corePoolSize, Integer maxPoolSize,
Boolean allowCoreThreadTimeOut, Duration keepAlive, Boolean awaitTermination,
Duration awaitTerminationPeriod, String threadNamePrefix, TaskDecorator taskDecorator,
Set<ThreadPoolTaskExecutorCustomizer> customizers) {
this.queueCapacity = queueCapacity;
this.corePoolSize = corePoolSize;
this.maxPoolSize = maxPoolSize;
this.allowCoreThreadTimeOut = allowCoreThreadTimeOut;
this.keepAlive = keepAlive;
this.awaitTermination = awaitTermination;
this.awaitTerminationPeriod = awaitTerminationPeriod;
this.threadNamePrefix = threadNamePrefix;
this.taskDecorator = taskDecorator;
this.customizers = customizers;
}
/**
* Set the capacity of the queue. An unbounded capacity does not increase the pool and
* therefore ignores {@link #maxPoolSize(int) maxPoolSize}.
* @param queueCapacity the queue capacity to set
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder queueCapacity(int queueCapacity) {
return new ThreadPoolTaskExecutorBuilder(queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set the core number of threads. Effectively that maximum number of threads as long
* as the queue is not full.
* <p>
* Core threads can grow and shrink if {@link #allowCoreThreadTimeOut(boolean)} is
* enabled.
* @param corePoolSize the core pool size to set
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder corePoolSize(int corePoolSize) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set the maximum allowed number of threads. When the {@link #queueCapacity(int)
* queue} is full, the pool can expand up to that size to accommodate the load.
* <p>
* If the {@link #queueCapacity(int) queue capacity} is unbounded, this setting is
* ignored.
* @param maxPoolSize the max pool size to set
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder maxPoolSize(int maxPoolSize) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set whether core threads are allowed to time out. When enabled, this enables
* dynamic growing and shrinking of the pool.
* @param allowCoreThreadTimeOut if core threads are allowed to time out
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder allowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set the time limit for which threads may remain idle before being terminated.
* @param keepAlive the keep alive to set
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder keepAlive(Duration keepAlive) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set whether the executor should wait for scheduled tasks to complete on shutdown,
* not interrupting running tasks and executing all tasks in the queue.
* @param awaitTermination whether the executor needs to wait for the tasks to
* complete on shutdown
* @return a new builder instance
* @see #awaitTerminationPeriod(Duration)
*/
public ThreadPoolTaskExecutorBuilder awaitTermination(boolean awaitTermination) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set the maximum time the executor is supposed to block on shutdown. When set, the
* executor blocks on shutdown in order to wait for remaining tasks to complete their
* execution before the rest of the container continues to shut down. This is
* particularly useful if your remaining tasks are likely to need access to other
* resources that are also managed by the container.
* @param awaitTerminationPeriod the await termination period to set
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder awaitTerminationPeriod(Duration awaitTerminationPeriod) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set the prefix to use for the names of newly created threads.
* @param threadNamePrefix the thread name prefix to set
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
threadNamePrefix, this.taskDecorator, this.customizers);
}
/**
* Set the {@link TaskDecorator} to use or {@code null} to not use any.
* @param taskDecorator the task decorator to use
* @return a new builder instance
*/
public ThreadPoolTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) {
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, taskDecorator, this.customizers);
}
/**
* Set the {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers}
* that should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are
* applied in the order that they were added after builder configuration has been
* applied. Setting this value will replace any previously configured customizers.
* @param customizers the customizers to set
* @return a new builder instance
* @see #additionalCustomizers(ThreadPoolTaskExecutorCustomizer...)
*/
public ThreadPoolTaskExecutorBuilder customizers(ThreadPoolTaskExecutorCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return customizers(Arrays.asList(customizers));
}
/**
* Set the {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers}
* that should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are
* applied in the order that they were added after builder configuration has been
* applied. Setting this value will replace any previously configured customizers.
* @param customizers the customizers to set
* @return a new builder instance
* @see #additionalCustomizers(ThreadPoolTaskExecutorCustomizer...)
*/
public ThreadPoolTaskExecutorBuilder customizers(Iterable<? extends ThreadPoolTaskExecutorCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, append(null, customizers));
}
/**
* Add {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers} that
* should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are applied in
* the order that they were added after builder configuration has been applied.
* @param customizers the customizers to add
* @return a new builder instance
* @see #customizers(ThreadPoolTaskExecutorCustomizer...)
*/
public ThreadPoolTaskExecutorBuilder additionalCustomizers(ThreadPoolTaskExecutorCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return additionalCustomizers(Arrays.asList(customizers));
}
/**
* Add {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers} that
* should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are applied in
* the order that they were added after builder configuration has been applied.
* @param customizers the customizers to add
* @return a new builder instance
* @see #customizers(ThreadPoolTaskExecutorCustomizer...)
*/
public ThreadPoolTaskExecutorBuilder additionalCustomizers(
Iterable<? extends ThreadPoolTaskExecutorCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize,
this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod,
this.threadNamePrefix, this.taskDecorator, append(this.customizers, customizers));
}
/**
* Build a new {@link ThreadPoolTaskExecutor} instance and configure it using this
* builder.
* @return a configured {@link ThreadPoolTaskExecutor} instance.
* @see #build(Class)
* @see #configure(ThreadPoolTaskExecutor)
*/
public ThreadPoolTaskExecutor build() {
return configure(new ThreadPoolTaskExecutor());
}
/**
* Build a new {@link ThreadPoolTaskExecutor} instance of the specified type and
* configure it using this builder.
* @param <T> the type of task executor
* @param taskExecutorClass the template type to create
* @return a configured {@link ThreadPoolTaskExecutor} instance.
* @see #build()
* @see #configure(ThreadPoolTaskExecutor)
*/
public <T extends ThreadPoolTaskExecutor> T build(Class<T> taskExecutorClass) {
return configure(BeanUtils.instantiateClass(taskExecutorClass));
}
/**
* Configure the provided {@link ThreadPoolTaskExecutor} instance using this builder.
* @param <T> the type of task executor
* @param taskExecutor the {@link ThreadPoolTaskExecutor} to configure
* @return the task executor instance
* @see #build()
* @see #build(Class)
*/
public <T extends ThreadPoolTaskExecutor> T configure(T taskExecutor) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.queueCapacity).to(taskExecutor::setQueueCapacity);
map.from(this.corePoolSize).to(taskExecutor::setCorePoolSize);
map.from(this.maxPoolSize).to(taskExecutor::setMaxPoolSize);
map.from(this.keepAlive).asInt(Duration::getSeconds).to(taskExecutor::setKeepAliveSeconds);
map.from(this.allowCoreThreadTimeOut).to(taskExecutor::setAllowCoreThreadTimeOut);
map.from(this.awaitTermination).to(taskExecutor::setWaitForTasksToCompleteOnShutdown);
map.from(this.awaitTerminationPeriod).as(Duration::toMillis).to(taskExecutor::setAwaitTerminationMillis);
map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix);
map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator);
if (!CollectionUtils.isEmpty(this.customizers)) {
this.customizers.forEach((customizer) -> customizer.customize(taskExecutor));
}
return taskExecutor;
}
private <T> Set<T> append(Set<T> set, Iterable<? extends T> additions) {
Set<T> result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet());
additions.forEach(result::add);
return Collections.unmodifiableSet(result);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2023 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.task;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* Callback interface that can be used to customize a {@link ThreadPoolTaskExecutor}.
*
* @author Stephane Nicoll
* @since 3.2.0
* @see ThreadPoolTaskExecutorBuilder
*/
@FunctionalInterface
public interface ThreadPoolTaskExecutorCustomizer {
/**
* Callback to customize a {@link ThreadPoolTaskExecutor} instance.
* @param taskExecutor the task executor to customize
*/
void customize(ThreadPoolTaskExecutor taskExecutor);
}

View File

@@ -37,6 +37,7 @@ import static org.mockito.Mockito.spy;
* @author Stephane Nicoll
* @author Filip Hrisafov
*/
@SuppressWarnings("removal")
class TaskExecutorBuilderTests {
private final TaskExecutorBuilder builder = new TaskExecutorBuilder();

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2012-2023 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.task;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link ThreadPoolTaskExecutorBuilder}.
*
* @author Stephane Nicoll
* @author Filip Hrisafov
*/
class ThreadPoolTaskExecutorBuilderTests {
private final ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder();
@Test
void poolSettingsShouldApply() {
ThreadPoolTaskExecutor executor = this.builder.queueCapacity(10)
.corePoolSize(4)
.maxPoolSize(8)
.allowCoreThreadTimeOut(true)
.keepAlive(Duration.ofMinutes(1))
.build();
assertThat(executor).hasFieldOrPropertyWithValue("queueCapacity", 10);
assertThat(executor.getCorePoolSize()).isEqualTo(4);
assertThat(executor.getMaxPoolSize()).isEqualTo(8);
assertThat(executor).hasFieldOrPropertyWithValue("allowCoreThreadTimeOut", true);
assertThat(executor.getKeepAliveSeconds()).isEqualTo(60);
}
@Test
void awaitTerminationShouldApply() {
ThreadPoolTaskExecutor executor = this.builder.awaitTermination(true).build();
assertThat(executor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true);
}
@Test
void awaitTerminationPeriodShouldApplyWithMillisecondPrecision() {
Duration period = Duration.ofMillis(50);
ThreadPoolTaskExecutor executor = this.builder.awaitTerminationPeriod(period).build();
assertThat(executor).hasFieldOrPropertyWithValue("awaitTerminationMillis", period.toMillis());
}
@Test
void threadNamePrefixShouldApply() {
ThreadPoolTaskExecutor executor = this.builder.threadNamePrefix("test-").build();
assertThat(executor.getThreadNamePrefix()).isEqualTo("test-");
}
@Test
void taskDecoratorShouldApply() {
TaskDecorator taskDecorator = mock(TaskDecorator.class);
ThreadPoolTaskExecutor executor = this.builder.taskDecorator(taskDecorator).build();
assertThat(executor).extracting("taskDecorator").isSameAs(taskDecorator);
}
@Test
void customizersWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.customizers((ThreadPoolTaskExecutorCustomizer[]) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void customizersCollectionWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.customizers((Set<ThreadPoolTaskExecutorCustomizer>) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void customizersShouldApply() {
ThreadPoolTaskExecutorCustomizer customizer = mock(ThreadPoolTaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = this.builder.customizers(customizer).build();
then(customizer).should().customize(executor);
}
@Test
void customizersShouldBeAppliedLast() {
TaskDecorator taskDecorator = mock(TaskDecorator.class);
ThreadPoolTaskExecutor executor = spy(new ThreadPoolTaskExecutor());
this.builder.queueCapacity(10)
.corePoolSize(4)
.maxPoolSize(8)
.allowCoreThreadTimeOut(true)
.keepAlive(Duration.ofMinutes(1))
.awaitTermination(true)
.awaitTerminationPeriod(Duration.ofSeconds(30))
.threadNamePrefix("test-")
.taskDecorator(taskDecorator)
.additionalCustomizers((taskExecutor) -> {
then(taskExecutor).should().setQueueCapacity(10);
then(taskExecutor).should().setCorePoolSize(4);
then(taskExecutor).should().setMaxPoolSize(8);
then(taskExecutor).should().setAllowCoreThreadTimeOut(true);
then(taskExecutor).should().setKeepAliveSeconds(60);
then(taskExecutor).should().setWaitForTasksToCompleteOnShutdown(true);
then(taskExecutor).should().setAwaitTerminationSeconds(30);
then(taskExecutor).should().setThreadNamePrefix("test-");
then(taskExecutor).should().setTaskDecorator(taskDecorator);
});
this.builder.configure(executor);
}
@Test
void customizersShouldReplaceExisting() {
ThreadPoolTaskExecutorCustomizer customizer1 = mock(ThreadPoolTaskExecutorCustomizer.class);
ThreadPoolTaskExecutorCustomizer customizer2 = mock(ThreadPoolTaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = this.builder.customizers(customizer1)
.customizers(Collections.singleton(customizer2))
.build();
then(customizer1).shouldHaveNoInteractions();
then(customizer2).should().customize(executor);
}
@Test
void additionalCustomizersWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.additionalCustomizers((ThreadPoolTaskExecutorCustomizer[]) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.additionalCustomizers((Set<ThreadPoolTaskExecutorCustomizer>) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void additionalCustomizersShouldAddToExisting() {
ThreadPoolTaskExecutorCustomizer customizer1 = mock(ThreadPoolTaskExecutorCustomizer.class);
ThreadPoolTaskExecutorCustomizer customizer2 = mock(ThreadPoolTaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = this.builder.customizers(customizer1)
.additionalCustomizers(customizer2)
.build();
then(customizer1).should().customize(executor);
then(customizer2).should().customize(executor);
}
}