From e4c38e59a9837cf63dd24f144f69eb81bc4fc7ad Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 31 Jul 2023 12:43:28 +0200 Subject: [PATCH] Implement SimpleAsyncTaskExecutorBuilder The SimpleAsyncTaskExecutorBuilder can be used to create SimpleAsyncTaskExecutor. It will be auto-configured into the context. SimpleAsyncTaskExecutorCustomizer can be used to customize the built SimpleAsyncTaskExecutor. If virtual threads are enabled: - SimpleAsyncTaskExecutor will use virtual threads - SimpleAsyncTaskExecutorBuilder will be used as the application task executor A new property 'spring.task.execution.simple.concurrency-limit' has been added to control the concurrency limit of the SimpleAsyncTaskExecutor Closes gh-35711 --- .../task/TaskExecutionAutoConfiguration.java | 1 + .../task/TaskExecutionProperties.java | 26 +- .../task/TaskExecutorConfigurations.java | 55 ++++- .../TaskExecutionAutoConfigurationTests.java | 44 +++- .../task/SimpleAsyncTaskExecutorBuilder.java | 222 ++++++++++++++++++ .../SimpleAsyncTaskExecutorCustomizer.java | 38 +++ .../SimpleAsyncTaskExecutorBuilderTests.java | 152 ++++++++++++ 7 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.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 935ef4dd17e..8d6ef29d676 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 @@ -37,6 +37,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @EnableConfigurationProperties(TaskExecutionProperties.class) @Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorBuilderConfiguration.class, + TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class }) public class TaskExecutionAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java index c8bcc17ce99..9530f198289 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.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. @@ -32,6 +32,8 @@ public class TaskExecutionProperties { private final Pool pool = new Pool(); + private final Simple simple = new Simple(); + private final Shutdown shutdown = new Shutdown(); /** @@ -39,6 +41,10 @@ public class TaskExecutionProperties { */ private String threadNamePrefix = "task-"; + public Simple getSimple() { + return this.simple; + } + public Pool getPool() { return this.pool; } @@ -55,6 +61,24 @@ public class TaskExecutionProperties { this.threadNamePrefix = threadNamePrefix; } + public static class Simple { + + /** + * Set the maximum number of parallel accesses allowed. -1 indicates no + * concurrency limit at all. + */ + private Integer concurrencyLimit; + + public Integer getConcurrencyLimit() { + return this.concurrencyLimit; + } + + public void setConcurrencyLimit(Integer concurrencyLimit) { + this.concurrencyLimit = concurrencyLimit; + } + + } + public static class Pool { /** 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 390e3b0f174..91ce9daef4a 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 @@ -23,6 +23,8 @@ 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.SimpleAsyncTaskExecutorBuilder; +import org.springframework.boot.task.SimpleAsyncTaskExecutorCustomizer; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; @@ -52,12 +54,8 @@ class TaskExecutorConfigurations { @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) - SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properties, - ObjectProvider taskDecorator) { - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(properties.getThreadNamePrefix()); - executor.setVirtualThreads(true); - taskDecorator.ifUnique(executor::setTaskDecorator); - return executor; + SimpleAsyncTaskExecutor applicationTaskExecutor(SimpleAsyncTaskExecutorBuilder builder) { + return builder.build(); } } @@ -144,4 +142,49 @@ class TaskExecutorConfigurations { } + @Configuration(proxyBeanMethods = false) + static class SimpleAsyncTaskExecutorBuilderConfiguration { + + private final TaskExecutionProperties properties; + + private final ObjectProvider taskExecutorCustomizers; + + private final ObjectProvider taskDecorator; + + SimpleAsyncTaskExecutorBuilderConfiguration(TaskExecutionProperties properties, + ObjectProvider taskExecutorCustomizers, + ObjectProvider taskDecorator) { + this.properties = properties; + this.taskExecutorCustomizers = taskExecutorCustomizers; + this.taskDecorator = taskDecorator; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) + SimpleAsyncTaskExecutorBuilder simpleAsyncTaskExecutorBuilder() { + return builder(); + } + + @Bean(name = "simpleAsyncTaskExecutorBuilder") + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskExecutorBuilder simpleAsyncTaskExecutorBuilderVirtualThreads() { + SimpleAsyncTaskExecutorBuilder builder = builder(); + builder = builder.virtualThreads(true); + return builder; + } + + private SimpleAsyncTaskExecutorBuilder builder() { + SimpleAsyncTaskExecutorBuilder builder = new SimpleAsyncTaskExecutorBuilder(); + builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix()); + builder = builder.customizers(this.taskExecutorCustomizers.orderedStream()::iterator); + builder = builder.taskDecorator(this.taskDecorator.getIfUnique()); + TaskExecutionProperties.Simple simple = this.properties.getSimple(); + builder = builder.concurrencyLimit(simple.getConcurrencyLimit()); + return builder; + } + + } + } 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 5e79e8eeb20..1a3149255ca 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 @@ -31,6 +31,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; @@ -50,6 +51,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -73,6 +75,7 @@ class TaskExecutionAutoConfigurationTests { assertThat(context).hasSingleBean(TaskExecutorBuilder.class); assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class); assertThat(context).hasSingleBean(ThreadPoolTaskExecutor.class); + assertThat(context).hasSingleBean(SimpleAsyncTaskExecutorBuilder.class); }); } @@ -106,6 +109,17 @@ class TaskExecutionAutoConfigurationTests { })); } + @Test + void simpleAsyncTaskExecutorBuilderShouldReadProperties() { + this.contextRunner + .withPropertyValues("spring.task.execution.thread-name-prefix=mytest-", + "spring.task.execution.simple.concurrency-limit=1") + .run(assertSimpleAsyncTaskExecutor((taskExecutor) -> { + assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1); + assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); + })); + } + @Test void threadPoolTaskExecutorBuilderShouldApplyCustomSettings() { this.contextRunner @@ -220,6 +234,23 @@ class TaskExecutionAutoConfigurationTests { }); } + @Test + void simpleAsyncTaskExecutorBuilderUsesPlatformThreadsByDefault() { + this.contextRunner.run((context) -> { + SimpleAsyncTaskExecutorBuilder builder = context.getBean(SimpleAsyncTaskExecutorBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", null); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void simpleAsyncTaskExecutorBuilderUsesVirtualThreadsWhenEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + SimpleAsyncTaskExecutorBuilder builder = context.getBean(SimpleAsyncTaskExecutorBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", true); + }); + } + @Test void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> { @@ -318,6 +349,15 @@ class TaskExecutionAutoConfigurationTests { }; } + private ContextConsumer assertSimpleAsyncTaskExecutor( + Consumer taskExecutor) { + return (context) -> { + assertThat(context).hasSingleBean(SimpleAsyncTaskExecutorBuilder.class); + SimpleAsyncTaskExecutorBuilder builder = context.getBean(SimpleAsyncTaskExecutorBuilder.class); + taskExecutor.accept(builder.build()); + }; + } + private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException { AtomicReference threadReference = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); @@ -326,7 +366,9 @@ class TaskExecutionAutoConfigurationTests { threadReference.set(currentThread); latch.countDown(); }); - latch.await(30, TimeUnit.SECONDS); + if (!latch.await(30, TimeUnit.SECONDS)) { + fail("Timeout while waiting for latch"); + } Thread thread = threadReference.get(); assertThat(thread).extracting("virtual").as("%s is virtual", thread).isEqualTo(true); return thread.getName(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java new file mode 100644 index 00000000000..12d71da1af1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java @@ -0,0 +1,222 @@ +/* + * 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.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.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link SimpleAsyncTaskExecutor}. + * Provides convenience methods to set common {@link SimpleAsyncTaskExecutor} settings and + * register {@link #taskDecorator(TaskDecorator)}). For advanced configuration, consider + * using {@link SimpleAsyncTaskExecutorCustomizer}. + *

+ * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link SimpleAsyncTaskExecutor} is needed. + * + * @author Stephane Nicoll + * @author Filip Hrisafov + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class SimpleAsyncTaskExecutorBuilder { + + private final Boolean virtualThreads; + + private final String threadNamePrefix; + + private final Integer concurrencyLimit; + + private final TaskDecorator taskDecorator; + + private final Set customizers; + + public SimpleAsyncTaskExecutorBuilder() { + this.virtualThreads = null; + this.threadNamePrefix = null; + this.concurrencyLimit = null; + this.taskDecorator = null; + this.customizers = null; + } + + private SimpleAsyncTaskExecutorBuilder(Boolean virtualThreads, String threadNamePrefix, Integer concurrencyLimit, + TaskDecorator taskDecorator, Set customizers) { + this.virtualThreads = virtualThreads; + this.threadNamePrefix = threadNamePrefix; + this.concurrencyLimit = concurrencyLimit; + this.taskDecorator = taskDecorator; + this.customizers = 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 SimpleAsyncTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) { + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, this.customizers); + } + + /** + * Set whether to use virtual threads. + * @param virtualThreads whether to use virtual threads + * @return a new builder instance + */ + public SimpleAsyncTaskExecutorBuilder virtualThreads(Boolean virtualThreads) { + return new SimpleAsyncTaskExecutorBuilder(virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, this.customizers); + } + + /** + * Set the concurrency limit. + * @param concurrencyLimit the concurrency limit + * @return a new builder instance + */ + public SimpleAsyncTaskExecutorBuilder concurrencyLimit(Integer concurrencyLimit) { + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, concurrencyLimit, + 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 SimpleAsyncTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) { + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + taskDecorator, this.customizers); + } + + /** + * Set the {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that + * should be applied to the {@link SimpleAsyncTaskExecutor}. 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(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder customizers(SimpleAsyncTaskExecutorCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that + * should be applied to the {@link SimpleAsyncTaskExecutor}. 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(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder customizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, append(null, customizers)); + } + + /** + * Add {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that should + * be applied to the {@link SimpleAsyncTaskExecutor}. 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(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder additionalCustomizers(SimpleAsyncTaskExecutorCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that should + * be applied to the {@link SimpleAsyncTaskExecutor}. 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(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder additionalCustomizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, append(this.customizers, customizers)); + } + + /** + * Build a new {@link SimpleAsyncTaskExecutor} instance and configure it using this + * builder. + * @return a configured {@link SimpleAsyncTaskExecutor} instance. + * @see #build(Class) + * @see #configure(SimpleAsyncTaskExecutor) + */ + public SimpleAsyncTaskExecutor build() { + return configure(new SimpleAsyncTaskExecutor()); + } + + /** + * Build a new {@link SimpleAsyncTaskExecutor} 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 SimpleAsyncTaskExecutor} instance. + * @see #build() + * @see #configure(SimpleAsyncTaskExecutor) + */ + public T build(Class taskExecutorClass) { + return configure(BeanUtils.instantiateClass(taskExecutorClass)); + } + + /** + * Configure the provided {@link SimpleAsyncTaskExecutor} instance using this builder. + * @param the type of task executor + * @param taskExecutor the {@link SimpleAsyncTaskExecutor} 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.virtualThreads).to(taskExecutor::setVirtualThreads); + map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix); + map.from(this.concurrencyLimit).to(taskExecutor::setConcurrencyLimit); + 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/SimpleAsyncTaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java new file mode 100644 index 00000000000..0f4218ecb4b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java @@ -0,0 +1,38 @@ +/* + * 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.core.task.SimpleAsyncTaskExecutor; + +/** + * Callback interface that can be used to customize a {@link SimpleAsyncTaskExecutor}. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + * @since 3.2.0 + * @see SimpleAsyncTaskExecutorBuilder + */ +@FunctionalInterface +public interface SimpleAsyncTaskExecutorCustomizer { + + /** + * Callback to customize a {@link SimpleAsyncTaskExecutor} instance. + * @param taskExecutor the task executor to customize + */ + void customize(SimpleAsyncTaskExecutor taskExecutor); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java new file mode 100644 index 00000000000..fe856cd9cba --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java @@ -0,0 +1,152 @@ +/* + * 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.lang.reflect.Field; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.util.ReflectionUtils; + +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 SimpleAsyncTaskExecutorBuilder}. + * + * @author Stephane Nicoll + * @author Filip Hrisafov + * @author Moritz Halbritter + */ +class SimpleAsyncTaskExecutorBuilderTests { + + private final SimpleAsyncTaskExecutorBuilder builder = new SimpleAsyncTaskExecutorBuilder(); + + @Test + void threadNamePrefixShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.threadNamePrefix("test-").build(); + assertThat(executor.getThreadNamePrefix()).isEqualTo("test-"); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void virtualThreadsShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.virtualThreads(true).build(); + Field field = ReflectionUtils.findField(SimpleAsyncTaskExecutor.class, "virtualThreadDelegate"); + assertThat(field).as("executor.virtualThreadDelegate").isNotNull(); + field.setAccessible(true); + Object virtualThreadDelegate = ReflectionUtils.getField(field, executor); + assertThat(virtualThreadDelegate).as("executor.virtualThreadDelegate").isNotNull(); + } + + @Test + void concurrencyLimitShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.concurrencyLimit(1).build(); + assertThat(executor.getConcurrencyLimit()).isEqualTo(1); + } + + @Test + void taskDecoratorShouldApply() { + TaskDecorator taskDecorator = mock(TaskDecorator.class); + SimpleAsyncTaskExecutor executor = this.builder.taskDecorator(taskDecorator).build(); + assertThat(executor).extracting("taskDecorator").isSameAs(taskDecorator); + } + + @Test + void customizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((SimpleAsyncTaskExecutorCustomizer[]) 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() { + SimpleAsyncTaskExecutorCustomizer customizer = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutor executor = this.builder.customizers(customizer).build(); + then(customizer).should().customize(executor); + } + + @Test + void customizersShouldBeAppliedLast() { + TaskDecorator taskDecorator = mock(TaskDecorator.class); + SimpleAsyncTaskExecutor executor = spy(new SimpleAsyncTaskExecutor()); + this.builder.threadNamePrefix("test-") + .virtualThreads(true) + .concurrencyLimit(1) + .taskDecorator(taskDecorator) + .additionalCustomizers((taskExecutor) -> { + then(taskExecutor).should().setConcurrencyLimit(1); + then(taskExecutor).should().setVirtualThreads(true); + then(taskExecutor).should().setThreadNamePrefix("test-"); + then(taskExecutor).should().setTaskDecorator(taskDecorator); + }); + this.builder.configure(executor); + } + + @Test + void customizersShouldReplaceExisting() { + SimpleAsyncTaskExecutorCustomizer customizer1 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutorCustomizer customizer2 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutor 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((SimpleAsyncTaskExecutorCustomizer[]) 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() { + SimpleAsyncTaskExecutorCustomizer customizer1 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutorCustomizer customizer2 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutor executor = this.builder.customizers(customizer1) + .additionalCustomizers(customizer2) + .build(); + then(customizer1).should().customize(executor); + then(customizer2).should().customize(executor); + } + +}