diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java index 9ca2706b611..71f20ef9e7a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java @@ -24,6 +24,7 @@ import javax.sql.DataSource; import io.rsocket.transport.netty.server.TcpServerTransport; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; @@ -43,6 +44,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.task.TaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -168,11 +170,18 @@ public class IntegrationAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnBean(TaskSchedulerBuilder.class) @ConditionalOnMissingBean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) + @SuppressWarnings("removal") protected static class IntegrationTaskSchedulerConfiguration { @Bean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) - public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { - return builder.build(); + public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder taskSchedulerBuilder, + ObjectProvider threadPoolTaskSchedulerBuilderProvider) { + ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder = threadPoolTaskSchedulerBuilderProvider + .getIfUnique(); + if (threadPoolTaskSchedulerBuilder != null) { + return threadPoolTaskSchedulerBuilder.build(); + } + return taskSchedulerBuilder.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java index d7969608de3..5d14f7bcdc8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java @@ -16,20 +16,14 @@ package org.springframework.boot.autoconfigure.task; -import java.util.concurrent.ScheduledExecutorService; - -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.task.TaskSchedulingProperties.Shutdown; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.task.TaskSchedulerBuilder; -import org.springframework.boot.task.TaskSchedulerCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.TaskManagementConfigUtils; @@ -38,38 +32,21 @@ import org.springframework.scheduling.config.TaskManagementConfigUtils; * {@link EnableAutoConfiguration Auto-configuration} for {@link TaskScheduler}. * * @author Stephane Nicoll + * @author Moritz Halbritter * @since 2.1.0 */ @ConditionalOnClass(ThreadPoolTaskScheduler.class) @AutoConfiguration(after = TaskExecutionAutoConfiguration.class) @EnableConfigurationProperties(TaskSchedulingProperties.class) +@Import({ TaskSchedulingConfigurations.ThreadPoolTaskSchedulerBuilderConfiguration.class, + TaskSchedulingConfigurations.TaskSchedulerBuilderConfiguration.class, + TaskSchedulingConfigurations.ThreadPoolTaskSchedulerConfiguration.class }) public class TaskSchedulingAutoConfiguration { - @Bean - @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) - @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) - public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { - return builder.build(); - } - @Bean @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() { return new ScheduledBeanLazyInitializationExcludeFilter(); } - @Bean - @ConditionalOnMissingBean - public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, - ObjectProvider taskSchedulerCustomizers) { - TaskSchedulerBuilder builder = new TaskSchedulerBuilder(); - builder = builder.poolSize(properties.getPool().getSize()); - Shutdown shutdown = properties.getShutdown(); - builder = builder.awaitTermination(shutdown.isAwaitTermination()); - builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); - builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); - builder = builder.customizers(taskSchedulerCustomizers); - return builder; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java new file mode 100644 index 00000000000..5375620e344 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java @@ -0,0 +1,108 @@ +/* + * 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.autoconfigure.task; + +import java.util.concurrent.ScheduledExecutorService; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.task.TaskSchedulerBuilder; +import org.springframework.boot.task.TaskSchedulerCustomizer; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.TaskManagementConfigUtils; + +/** + * {@link TaskScheduler} configurations to be imported by + * {@link TaskSchedulingAutoConfiguration} in a specific order. + * + * @author Moritz Halbritter + */ +class TaskSchedulingConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) + @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) + @SuppressWarnings("removal") + static class ThreadPoolTaskSchedulerConfiguration { + + @Bean + ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder taskSchedulerBuilder, + ObjectProvider threadPoolTaskSchedulerBuilderProvider) { + ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder = threadPoolTaskSchedulerBuilderProvider + .getIfUnique(); + if (threadPoolTaskSchedulerBuilder != null) { + return threadPoolTaskSchedulerBuilder.build(); + } + return taskSchedulerBuilder.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class TaskSchedulerBuilderConfiguration { + + @Bean + @ConditionalOnMissingBean + TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, + ObjectProvider taskSchedulerCustomizers) { + TaskSchedulerBuilder builder = new TaskSchedulerBuilder(); + builder = builder.poolSize(properties.getPool().getSize()); + TaskSchedulingProperties.Shutdown shutdown = properties.getShutdown(); + builder = builder.awaitTermination(shutdown.isAwaitTermination()); + builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); + builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); + builder = builder.customizers(taskSchedulerCustomizers); + return builder; + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class ThreadPoolTaskSchedulerBuilderConfiguration { + + @Bean + @ConditionalOnMissingBean({ TaskSchedulerBuilder.class, ThreadPoolTaskSchedulerBuilder.class }) + ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder(TaskSchedulingProperties properties, + ObjectProvider threadPoolTaskSchedulerCustomizers, + ObjectProvider taskSchedulerCustomizers) { + TaskSchedulingProperties.Shutdown shutdown = properties.getShutdown(); + ThreadPoolTaskSchedulerBuilder builder = new ThreadPoolTaskSchedulerBuilder(); + builder = builder.poolSize(properties.getPool().getSize()); + builder = builder.awaitTermination(shutdown.isAwaitTermination()); + builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); + builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); + builder = builder.customizers(threadPoolTaskSchedulerCustomizers); + // Apply the deprecated TaskSchedulerCustomizers, too + builder = builder.additionalCustomizers(taskSchedulerCustomizers.orderedStream().map(this::adapt).toList()); + return builder; + } + + private ThreadPoolTaskSchedulerCustomizer adapt(TaskSchedulerCustomizer customizer) { + return customizer::customize; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 8d42ccd0517..0898ec2beeb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -31,7 +31,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.task.TaskSchedulerBuilder; import org.springframework.boot.task.TaskSchedulerCustomizer; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerCustomizer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,7 +52,9 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link TaskSchedulingAutoConfiguration}. * * @author Stephane Nicoll + * @author Moritz Halbritter */ +@SuppressWarnings("removal") class TaskSchedulingAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -67,6 +72,26 @@ class TaskSchedulingAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(ScheduledBeanLazyInitializationExcludeFilter.class)); } + @Test + void shouldSupplyBeans() { + this.contextRunner.withUserConfiguration(SchedulingConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(TaskSchedulerBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskSchedulerBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskScheduler.class); + }); + } + + @Test + void shouldNotSupplyThreadPoolTaskSchedulerBuilderIfCustomTaskSchedulerBuilderIsPresent() { + this.contextRunner.withUserConfiguration(SchedulingConfiguration.class) + .withBean(TaskSchedulerBuilder.class, TaskSchedulerBuilder::new) + .run((context) -> { + assertThat(context).hasSingleBean(TaskSchedulerBuilder.class); + assertThat(context).doesNotHaveBean(ThreadPoolTaskSchedulerBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskScheduler.class); + }); + } + @Test void enableSchedulingWithNoTaskExecutorAutoConfiguresOne() { this.contextRunner @@ -86,7 +111,7 @@ class TaskSchedulingAutoConfigurationTests { } @Test - void enableSchedulingWithNoTaskExecutorAppliesCustomizers() { + void enableSchedulingWithNoTaskExecutorAppliesTaskSchedulerCustomizers() { this.contextRunner.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-") .withUserConfiguration(SchedulingConfiguration.class, TaskSchedulerCustomizerConfiguration.class) .run((context) -> { @@ -97,6 +122,18 @@ class TaskSchedulingAutoConfigurationTests { }); } + @Test + void enableSchedulingWithNoTaskExecutorAppliesCustomizers() { + this.contextRunner.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-") + .withUserConfiguration(SchedulingConfiguration.class, ThreadPoolTaskSchedulerCustomizerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(TaskExecutor.class); + TestBean bean = context.getBean(TestBean.class); + assertThat(bean.latch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(bean.threadNames).allMatch((name) -> name.contains("customized-scheduler-")); + }); + } + @Test void enableSchedulingWithExistingTaskSchedulerBacksOff() { this.contextRunner.withUserConfiguration(SchedulingConfiguration.class, TaskSchedulerConfiguration.class) @@ -175,6 +212,16 @@ class TaskSchedulingAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ThreadPoolTaskSchedulerCustomizerConfiguration { + + @Bean + ThreadPoolTaskSchedulerCustomizer testTaskSchedulerCustomizer() { + return ((taskScheduler) -> taskScheduler.setThreadNamePrefix("customized-scheduler-")); + } + + } + @Configuration(proxyBeanMethods = false) static class SchedulingConfigurerConfiguration implements SchedulingConfigurer { 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 dadb185ffc0..5b3ed7f031d 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 @@ -49,5 +49,5 @@ The thread pool uses one thread by default and its settings can be fine-tuned us size: 2 ---- -A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. +A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `ThreadPoolTaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. The `SimpleAsyncTaskExecutorBuilder` is auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`). diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java index 9ec2c3e6aee..65c385d0c5a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.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. @@ -38,7 +38,11 @@ import org.springframework.util.CollectionUtils; * * @author Stephane Nicoll * @since 2.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link ThreadPoolTaskSchedulerBuilder} */ +@Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("removal") public class TaskSchedulerBuilder { private final Integer poolSize; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java index 7c5252c6866..8acf391a42a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.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. @@ -23,8 +23,11 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; * * @author Stephane Nicoll * @since 2.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link ThreadPoolTaskSchedulerCustomizer} */ @FunctionalInterface +@Deprecated(since = "3.2.0", forRemoval = true) public interface TaskSchedulerCustomizer { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java new file mode 100644 index 00000000000..a36e48308ee --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java @@ -0,0 +1,214 @@ +/* + * 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.boot.context.properties.PropertyMapper; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link ThreadPoolTaskScheduler}. + * Provides convenience methods to set common {@link ThreadPoolTaskScheduler} settings. + * For advanced configuration, consider using {@link ThreadPoolTaskSchedulerCustomizer}. + *

+ * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link ThreadPoolTaskScheduler} is needed. + * + * @author Stephane Nicoll + * @since 3.2.0 + */ +public class ThreadPoolTaskSchedulerBuilder { + + private final Integer poolSize; + + private final Boolean awaitTermination; + + private final Duration awaitTerminationPeriod; + + private final String threadNamePrefix; + + private final Set customizers; + + public ThreadPoolTaskSchedulerBuilder() { + this.poolSize = null; + this.awaitTermination = null; + this.awaitTerminationPeriod = null; + this.threadNamePrefix = null; + this.customizers = null; + } + + public ThreadPoolTaskSchedulerBuilder(Integer poolSize, Boolean awaitTermination, Duration awaitTerminationPeriod, + String threadNamePrefix, Set taskSchedulerCustomizers) { + this.poolSize = poolSize; + this.awaitTermination = awaitTermination; + this.awaitTerminationPeriod = awaitTerminationPeriod; + this.threadNamePrefix = threadNamePrefix; + this.customizers = taskSchedulerCustomizers; + } + + /** + * Set the maximum allowed number of threads. + * @param poolSize the pool size to set + * @return a new builder instance + */ + public ThreadPoolTaskSchedulerBuilder poolSize(int poolSize) { + return new ThreadPoolTaskSchedulerBuilder(poolSize, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, 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 ThreadPoolTaskSchedulerBuilder awaitTermination(boolean awaitTermination) { + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, 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 ThreadPoolTaskSchedulerBuilder awaitTerminationPeriod(Duration awaitTerminationPeriod) { + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, awaitTerminationPeriod, + this.threadNamePrefix, 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 ThreadPoolTaskSchedulerBuilder threadNamePrefix(String threadNamePrefix) { + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, this.awaitTerminationPeriod, + threadNamePrefix, this.customizers); + } + + /** + * Set the {@link ThreadPoolTaskSchedulerCustomizer + * threadPoolTaskSchedulerCustomizers} that should be applied to the + * {@link ThreadPoolTaskScheduler}. 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(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder customizers(ThreadPoolTaskSchedulerCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link ThreadPoolTaskSchedulerCustomizer + * threadPoolTaskSchedulerCustomizers} that should be applied to the + * {@link ThreadPoolTaskScheduler}. 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(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder customizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, append(null, customizers)); + } + + /** + * Add {@link ThreadPoolTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} + * that should be applied to the {@link ThreadPoolTaskScheduler}. 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(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder additionalCustomizers(ThreadPoolTaskSchedulerCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link ThreadPoolTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} + * that should be applied to the {@link ThreadPoolTaskScheduler}. 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(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder additionalCustomizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, append(this.customizers, customizers)); + } + + /** + * Build a new {@link ThreadPoolTaskScheduler} instance and configure it using this + * builder. + * @return a configured {@link ThreadPoolTaskScheduler} instance. + * @see #configure(ThreadPoolTaskScheduler) + */ + public ThreadPoolTaskScheduler build() { + return configure(new ThreadPoolTaskScheduler()); + } + + /** + * Configure the provided {@link ThreadPoolTaskScheduler} instance using this builder. + * @param the type of task scheduler + * @param taskScheduler the {@link ThreadPoolTaskScheduler} to configure + * @return the task scheduler instance + * @see #build() + */ + public T configure(T taskScheduler) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.poolSize).to(taskScheduler::setPoolSize); + map.from(this.awaitTermination).to(taskScheduler::setWaitForTasksToCompleteOnShutdown); + map.from(this.awaitTerminationPeriod).asInt(Duration::getSeconds).to(taskScheduler::setAwaitTerminationSeconds); + map.from(this.threadNamePrefix).to(taskScheduler::setThreadNamePrefix); + if (!CollectionUtils.isEmpty(this.customizers)) { + this.customizers.forEach((customizer) -> customizer.customize(taskScheduler)); + } + return taskScheduler; + } + + 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/ThreadPoolTaskSchedulerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerCustomizer.java new file mode 100644 index 00000000000..0e7cc44458e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerCustomizer.java @@ -0,0 +1,36 @@ +/* + * 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.ThreadPoolTaskScheduler; + +/** + * Callback interface that can be used to customize a {@link ThreadPoolTaskScheduler}. + * + * @author Stephane Nicoll + * @since 3.2.0 + */ +@FunctionalInterface +public interface ThreadPoolTaskSchedulerCustomizer { + + /** + * Callback to customize a {@link ThreadPoolTaskScheduler} instance. + * @param taskScheduler the task scheduler to customize + */ + void customize(ThreadPoolTaskScheduler taskScheduler); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java index e07573910c1..095e8fda4ed 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.spy; * * @author Stephane Nicoll */ +@SuppressWarnings("removal") class TaskSchedulerBuilderTests { private final TaskSchedulerBuilder builder = new TaskSchedulerBuilder(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java new file mode 100644 index 00000000000..11b4f15f49a --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java @@ -0,0 +1,134 @@ +/* + * 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.scheduling.concurrent.ThreadPoolTaskScheduler; + +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 ThreadPoolTaskSchedulerBuilder}. + * + * @author Stephane Nicoll + */ +class ThreadPoolTaskSchedulerBuilderTests { + + private final ThreadPoolTaskSchedulerBuilder builder = new ThreadPoolTaskSchedulerBuilder(); + + @Test + void poolSettingsShouldApply() { + ThreadPoolTaskScheduler scheduler = this.builder.poolSize(4).build(); + assertThat(scheduler.getPoolSize()).isEqualTo(4); + } + + @Test + void awaitTerminationShouldApply() { + ThreadPoolTaskScheduler executor = this.builder.awaitTermination(true).build(); + assertThat(executor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); + } + + @Test + void awaitTerminationPeriodShouldApply() { + Duration period = Duration.ofMinutes(1); + ThreadPoolTaskScheduler executor = this.builder.awaitTerminationPeriod(period).build(); + assertThat(executor).hasFieldOrPropertyWithValue("awaitTerminationMillis", period.toMillis()); + } + + @Test + void threadNamePrefixShouldApply() { + ThreadPoolTaskScheduler scheduler = this.builder.threadNamePrefix("test-").build(); + assertThat(scheduler.getThreadNamePrefix()).isEqualTo("test-"); + } + + @Test + void customizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((ThreadPoolTaskSchedulerCustomizer[]) 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() { + ThreadPoolTaskSchedulerCustomizer customizer = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskScheduler scheduler = this.builder.customizers(customizer).build(); + then(customizer).should().customize(scheduler); + } + + @Test + void customizersShouldBeAppliedLast() { + ThreadPoolTaskScheduler scheduler = spy(new ThreadPoolTaskScheduler()); + this.builder.poolSize(4).threadNamePrefix("test-").additionalCustomizers((taskScheduler) -> { + then(taskScheduler).should().setPoolSize(4); + then(taskScheduler).should().setThreadNamePrefix("test-"); + }); + this.builder.configure(scheduler); + } + + @Test + void customizersShouldReplaceExisting() { + ThreadPoolTaskSchedulerCustomizer customizer1 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskSchedulerCustomizer customizer2 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskScheduler scheduler = this.builder.customizers(customizer1) + .customizers(Collections.singleton(customizer2)) + .build(); + then(customizer1).shouldHaveNoInteractions(); + then(customizer2).should().customize(scheduler); + } + + @Test + void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((ThreadPoolTaskSchedulerCustomizer[]) 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() { + ThreadPoolTaskSchedulerCustomizer customizer1 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskSchedulerCustomizer customizer2 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskScheduler scheduler = this.builder.customizers(customizer1) + .additionalCustomizers(customizer2) + .build(); + then(customizer1).should().customize(scheduler); + then(customizer2).should().customize(scheduler); + } + +}