From 32c91af440544effe40516927cc768c3c401a412 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 31 Jul 2023 09:21:43 +0200 Subject: [PATCH] Add ThreadPoolTaskExecutorBuilder and deprecate TaskExecutorBuilder Closes gh-36637 --- .../task/TaskExecutionAutoConfiguration.java | 33 +- .../task/TaskExecutorConfigurations.java | 78 ++++- .../TaskExecutionAutoConfigurationTests.java | 124 ++++++- .../task-execution-and-scheduling.adoc | 4 +- .../boot/task/TaskExecutorBuilder.java | 6 +- .../boot/task/TaskExecutorCustomizer.java | 5 +- .../task/ThreadPoolTaskExecutorBuilder.java | 326 ++++++++++++++++++ .../ThreadPoolTaskExecutorCustomizer.java | 37 ++ .../boot/task/TaskExecutorBuilderTests.java | 1 + .../ThreadPoolTaskExecutorBuilderTests.java | 169 +++++++++ 10 files changed, 741 insertions(+), 42 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index c0530797c4..935ef4dd17 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -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 taskExecutorCustomizers, - ObjectProvider 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; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index 2e9646b90a..390e3b0f17 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -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 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 taskExecutorCustomizers, + ObjectProvider 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 threadPoolTaskExecutorCustomizers, + ObjectProvider taskExecutorCustomizers, + ObjectProvider 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; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 8a1e09e922..5e79e8eeb2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -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 assertTaskExecutor( Consumer taskExecutor) { return (context) -> { @@ -221,6 +309,15 @@ class TaskExecutionAutoConfigurationTests { }; } + private ContextConsumer assertThreadPoolTaskExecutor( + Consumer 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 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 { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 9ae7213855..6126919354 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java index 34b45a8317..304b9ce932 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java index 4ceed9047b..0ff969caab 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java @@ -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 { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java new file mode 100644 index 0000000000..2609245832 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java @@ -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}. + *

+ * 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 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 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. + *

+ * 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. + *

+ * 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 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 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 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 build(Class taskExecutorClass) { + return configure(BeanUtils.instantiateClass(taskExecutorClass)); + } + + /** + * Configure the provided {@link ThreadPoolTaskExecutor} instance using this builder. + * @param the type of task executor + * @param taskExecutor the {@link ThreadPoolTaskExecutor} to configure + * @return the task executor instance + * @see #build() + * @see #build(Class) + */ + public 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 Set append(Set set, Iterable additions) { + Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); + additions.forEach(result::add); + return Collections.unmodifiableSet(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java new file mode 100644 index 0000000000..c81c5bfe79 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java @@ -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); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java index 52c205e8ed..7df760b63f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java @@ -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(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java new file mode 100644 index 0000000000..b57ffc6905 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java @@ -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) 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) 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); + } + +}