Skip to content

Commit 13bd614

Browse files
committed
Allow configuring a TaskExecutor even if an Executor is present
This commit updates `TaskExecutionAutoConfiguration` to permit the auto-configuration of a `TaskExecutor` even if a user-defined `Executor` bean is present. Such `Executor` may have been created for totally unrelated reason, and it may or may not be an `AsyncTaskExecutor`. The default behavior has not changed, but this commit provides a new property, `spring.task.execution.mode` that can be set to `force` to auto-configure the `TaskExecutor` anyway. Because this mode made it so that two `Executor` will be present in the context, this commit also automatically configures an `AsyncConfigurer` if none is present already to make sure task processing uses the auto-configured TaskExecutor. Closes gh-44659
1 parent ac55094 commit 13bd614

File tree

4 files changed

+216
-34
lines changed

4 files changed

+216
-34
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,11 @@ public class TaskExecutionProperties {
3737

3838
private final Shutdown shutdown = new Shutdown();
3939

40+
/**
41+
* Determine when the task executor is to be created.
42+
*/
43+
private Mode mode = Mode.AUTO;
44+
4045
/**
4146
* Prefix to use for the names of newly created threads.
4247
*/
@@ -54,6 +59,14 @@ public Shutdown getShutdown() {
5459
return this.shutdown;
5560
}
5661

62+
public Mode getMode() {
63+
return this.mode;
64+
}
65+
66+
public void setMode(Mode mode) {
67+
this.mode = mode;
68+
}
69+
5770
public String getThreadNamePrefix() {
5871
return this.threadNamePrefix;
5972
}
@@ -209,4 +222,23 @@ public void setAwaitTerminationPeriod(Duration awaitTerminationPeriod) {
209222

210223
}
211224

225+
/**
226+
* Determine when the task executor is to be created.
227+
*
228+
* @since 3.5.0
229+
*/
230+
public enum Mode {
231+
232+
/**
233+
* Create the task executor if no user-defined executor is present.
234+
*/
235+
AUTO,
236+
237+
/**
238+
* Create the task executor even if a user-defined executor is present.
239+
*/
240+
FORCE
241+
242+
}
243+
212244
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java

+48-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,21 +18,27 @@
1818

1919
import java.util.concurrent.Executor;
2020

21+
import org.springframework.beans.factory.BeanFactory;
2122
import org.springframework.beans.factory.ObjectProvider;
23+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
2224
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2326
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
2427
import org.springframework.boot.autoconfigure.thread.Threading;
2528
import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
2629
import org.springframework.boot.task.SimpleAsyncTaskExecutorCustomizer;
2730
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
2831
import org.springframework.boot.task.ThreadPoolTaskExecutorCustomizer;
2932
import org.springframework.context.annotation.Bean;
33+
import org.springframework.context.annotation.Conditional;
3034
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.context.annotation.Import;
3136
import org.springframework.context.annotation.Lazy;
3237
import org.springframework.core.task.SimpleAsyncTaskExecutor;
3338
import org.springframework.core.task.TaskDecorator;
3439
import org.springframework.core.task.TaskExecutor;
3540
import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
41+
import org.springframework.scheduling.annotation.AsyncConfigurer;
3642
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
3743

3844
/**
@@ -46,19 +52,18 @@
4652
class TaskExecutorConfigurations {
4753

4854
@Configuration(proxyBeanMethods = false)
49-
@ConditionalOnMissingBean(Executor.class)
55+
@Conditional(OnExecutorCondition.class)
56+
@Import(AsyncConfigurerConfiguration.class)
5057
static class TaskExecutorConfiguration {
5158

52-
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
53-
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
59+
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
5460
@ConditionalOnThreading(Threading.VIRTUAL)
5561
SimpleAsyncTaskExecutor applicationTaskExecutorVirtualThreads(SimpleAsyncTaskExecutorBuilder builder) {
5662
return builder.build();
5763
}
5864

65+
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
5966
@Lazy
60-
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
61-
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
6267
@ConditionalOnThreading(Threading.PLATFORM)
6368
ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder) {
6469
return threadPoolTaskExecutorBuilder.build();
@@ -140,4 +145,41 @@ private SimpleAsyncTaskExecutorBuilder builder() {
140145

141146
}
142147

148+
@Configuration(proxyBeanMethods = false)
149+
@ConditionalOnMissingBean(name = AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME,
150+
value = AsyncConfigurer.class)
151+
static class AsyncConfigurerConfiguration {
152+
153+
@Bean
154+
@ConditionalOnMissingBean
155+
AsyncConfigurer applicationTaskExecutorAsyncConfigurer(BeanFactory beanFactory) {
156+
return new AsyncConfigurer() {
157+
@Override
158+
public Executor getAsyncExecutor() {
159+
return beanFactory.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
160+
Executor.class);
161+
}
162+
};
163+
}
164+
165+
}
166+
167+
static class OnExecutorCondition extends AnyNestedCondition {
168+
169+
OnExecutorCondition() {
170+
super(ConfigurationPhase.REGISTER_BEAN);
171+
}
172+
173+
@ConditionalOnMissingBean(Executor.class)
174+
private static final class ExecutorBeanCondition {
175+
176+
}
177+
178+
@ConditionalOnProperty(value = "spring.task.execution.mode", havingValue = "force")
179+
private static final class ModelCondition {
180+
181+
}
182+
183+
}
184+
143185
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java

+112-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.junit.jupiter.api.extension.ExtendWith;
3131

3232
import org.springframework.beans.factory.config.BeanDefinition;
33+
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
3334
import org.springframework.boot.autoconfigure.AutoConfigurations;
3435
import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
3536
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
@@ -44,6 +45,7 @@
4445
import org.springframework.core.task.TaskDecorator;
4546
import org.springframework.core.task.TaskExecutor;
4647
import org.springframework.scheduling.annotation.Async;
48+
import org.springframework.scheduling.annotation.AsyncConfigurer;
4749
import org.springframework.scheduling.annotation.EnableAsync;
4850
import org.springframework.scheduling.annotation.EnableScheduling;
4951
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -204,16 +206,43 @@ void simpleAsyncTaskExecutorBuilderUsesVirtualThreadsWhenEnabled() {
204206

205207
@Test
206208
void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
207-
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> {
209+
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> {
208210
assertThat(context).hasSingleBean(Executor.class);
209211
assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor"));
210212
});
211213
}
212214

215+
@Test
216+
void taskExecutorWhenModeIsAutoAndHasCustomTaskExecutorShouldBackOff() {
217+
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new)
218+
.withPropertyValues("spring.task.execution.mode=auto")
219+
.run((context) -> {
220+
assertThat(context).hasSingleBean(Executor.class);
221+
assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor"));
222+
});
223+
}
224+
225+
@Test
226+
void taskExecutorWhenModeIsForceAndHasCustomTaskExecutorShouldCreateApplicationTaskExecutor() {
227+
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new)
228+
.withPropertyValues("spring.task.execution.mode=force")
229+
.run((context) -> assertThat(context.getBeansOfType(Executor.class)).hasSize(2)
230+
.containsKeys("customTaskExecutor", "applicationTaskExecutor"));
231+
}
232+
233+
@Test
234+
void taskExecutorWhenModeIsForceAndHasCustomTaskExecutorWithReservedNameShouldThrowException() {
235+
this.contextRunner.withBean("applicationTaskExecutor", Executor.class, SyncTaskExecutor::new)
236+
.withPropertyValues("spring.task.execution.mode=force")
237+
.run((context) -> assertThat(context).hasFailed()
238+
.getFailure()
239+
.isInstanceOf(BeanDefinitionOverrideException.class));
240+
}
241+
213242
@Test
214243
@EnabledForJreRange(min = JRE.JAVA_21)
215244
void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() {
216-
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class)
245+
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new)
217246
.withPropertyValues("spring.threads.virtual.enabled=true")
218247
.run((context) -> {
219248
assertThat(context).hasSingleBean(Executor.class);
@@ -223,25 +252,101 @@ void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTa
223252

224253
@Test
225254
void enableAsyncUsesAutoConfiguredOneByDefault() {
226-
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=task-test-")
255+
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
227256
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
228257
.run((context) -> {
258+
assertThat(context).hasSingleBean(AsyncConfigurer.class);
229259
assertThat(context).hasSingleBean(TaskExecutor.class);
230260
TestBean bean = context.getBean(TestBean.class);
231261
String text = bean.echo("something").get();
232-
assertThat(text).contains("task-test-").contains("something");
262+
assertThat(text).contains("auto-task-").contains("something");
263+
});
264+
}
265+
266+
@Test
267+
void enableAsyncUsesCustomExecutorIfPresent() {
268+
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
269+
.withBean("customTaskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
270+
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
271+
.run((context) -> {
272+
assertThat(context).doesNotHaveBean(AsyncConfigurer.class);
273+
assertThat(context).hasSingleBean(Executor.class);
274+
TestBean bean = context.getBean(TestBean.class);
275+
String text = bean.echo("something").get();
276+
assertThat(text).contains("custom-task-").contains("something");
277+
});
278+
}
279+
280+
@Test
281+
void enableAsyncUsesAutoConfiguredExecutorWhenModeIsForceAndHasCustomTaskExecutor() {
282+
this.contextRunner
283+
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
284+
"spring.task.execution.mode=force")
285+
.withBean("customTaskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
286+
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
287+
.run((context) -> {
288+
assertThat(context).hasSingleBean(AsyncConfigurer.class);
289+
assertThat(context.getBeansOfType(Executor.class)).hasSize(2);
290+
TestBean bean = context.getBean(TestBean.class);
291+
String text = bean.echo("something").get();
292+
assertThat(text).contains("auto-task-").contains("something");
233293
});
234294
}
235295

296+
@Test
297+
void enableAsyncUsesCustomExecutorWhenModeIsForceAndHasCustomTaskExecutorWithReservedName() {
298+
this.contextRunner
299+
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
300+
"spring.task.execution.mode=force")
301+
.withBean("taskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
302+
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
303+
.run((context) -> {
304+
assertThat(context).doesNotHaveBean(AsyncConfigurer.class);
305+
assertThat(context.getBeansOfType(Executor.class)).hasSize(2);
306+
TestBean bean = context.getBean(TestBean.class);
307+
String text = bean.echo("something").get();
308+
assertThat(text).contains("custom-task-").contains("something");
309+
});
310+
}
311+
312+
@Test
313+
void enableAsyncUsesAsyncConfigurerWhenModeIsForce() {
314+
this.contextRunner
315+
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
316+
"spring.task.execution.mode=force")
317+
.withBean("taskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
318+
.withBean("customAsyncConfigurer", AsyncConfigurer.class, () -> new AsyncConfigurer() {
319+
@Override
320+
public Executor getAsyncExecutor() {
321+
return createCustomAsyncExecutor("async-task-");
322+
}
323+
})
324+
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
325+
.run((context) -> {
326+
assertThat(context).hasSingleBean(AsyncConfigurer.class);
327+
assertThat(context.getBeansOfType(Executor.class)).hasSize(2)
328+
.containsOnlyKeys("taskExecutor", "applicationTaskExecutor");
329+
TestBean bean = context.getBean(TestBean.class);
330+
String text = bean.echo("something").get();
331+
assertThat(text).contains("async-task-").contains("something");
332+
});
333+
}
334+
335+
private Executor createCustomAsyncExecutor(String threadNamePrefix) {
336+
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
337+
executor.setThreadNamePrefix(threadNamePrefix);
338+
return executor;
339+
}
340+
236341
@Test
237342
void enableAsyncUsesAutoConfiguredOneByDefaultEvenThoughSchedulingIsConfigured() {
238-
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=task-test-")
343+
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
239344
.withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class))
240345
.withUserConfiguration(AsyncConfiguration.class, SchedulingConfiguration.class, TestBean.class)
241346
.run((context) -> {
242347
TestBean bean = context.getBean(TestBean.class);
243348
String text = bean.echo("something").get();
244-
assertThat(text).contains("task-test-").contains("something");
349+
assertThat(text).contains("auto-task-").contains("something");
245350
});
246351
}
247352

@@ -299,16 +404,6 @@ TaskDecorator mockTaskDecorator() {
299404

300405
}
301406

302-
@Configuration(proxyBeanMethods = false)
303-
static class CustomTaskExecutorConfig {
304-
305-
@Bean
306-
Executor customTaskExecutor() {
307-
return new SyncTaskExecutor();
308-
}
309-
310-
}
311-
312407
@Configuration(proxyBeanMethods = false)
313408
@EnableAsync
314409
static class AsyncConfiguration {

0 commit comments

Comments
 (0)