Skip to content

Commit b0e2f2c

Browse files
committed
Configure application task executor with custom AsyncConfigurer
Previously, any user that defines an AsyncConfigurer should provide the async executor as well, given that our auto-configuration no longer defines a "taskExecutor" bean. This commit extends our auto-configuration of async processing to use the "applicationTaskExecutor" bean name transparently if a user has configured an AsyncConfigurer, but did not override the getAsyncExecutor method. Closes gh-47897
1 parent f11e3f3 commit b0e2f2c

File tree

2 files changed

+123
-9
lines changed

2 files changed

+123
-9
lines changed

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

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121

2222
import org.jspecify.annotations.Nullable;
2323

24+
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
25+
import org.springframework.beans.BeansException;
2426
import org.springframework.beans.factory.BeanFactory;
2527
import org.springframework.beans.factory.ObjectProvider;
2628
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
29+
import org.springframework.beans.factory.config.BeanPostProcessor;
2730
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2832
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2933
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3034
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
@@ -66,7 +70,7 @@ class TaskExecutorConfigurations {
6670

6771
@Configuration(proxyBeanMethods = false)
6872
@Conditional(OnExecutorCondition.class)
69-
@Import(AsyncConfigurerConfiguration.class)
73+
@Import({ AsyncConfigurerWrapperConfiguration.class, AsyncConfigurerConfiguration.class })
7074
static class TaskExecutorConfiguration {
7175

7276
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@@ -161,23 +165,37 @@ private SimpleAsyncTaskExecutorBuilder builder() {
161165
}
162166

163167
@Configuration(proxyBeanMethods = false)
164-
@ConditionalOnMissingBean(AsyncConfigurer.class)
165-
static class AsyncConfigurerConfiguration {
168+
@ConditionalOnBean(AsyncConfigurer.class)
169+
static class AsyncConfigurerWrapperConfiguration {
166170

167171
@Bean
168-
@ConditionalOnMissingBean
169-
AsyncConfigurer applicationTaskExecutorAsyncConfigurer(BeanFactory beanFactory) {
170-
return new AsyncConfigurer() {
172+
static BeanPostProcessor applicationTaskExecutorAsyncConfigurerBeanPostProcessor(
173+
ObjectProvider<BeanFactory> beanFactory) {
174+
return new BeanPostProcessor() {
171175
@Override
172-
public Executor getAsyncExecutor() {
173-
return beanFactory.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
174-
Executor.class);
176+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
177+
if (bean instanceof AsyncConfigurer asyncConfigurer
178+
&& !(bean instanceof ApplicationTaskExecutorAsyncConfigurer)) {
179+
return new ApplicationTaskExecutorAsyncConfigurer(beanFactory.getObject(), asyncConfigurer);
180+
}
181+
return bean;
175182
}
176183
};
177184
}
178185

179186
}
180187

188+
@Configuration(proxyBeanMethods = false)
189+
@ConditionalOnMissingBean(AsyncConfigurer.class)
190+
static class AsyncConfigurerConfiguration {
191+
192+
@Bean
193+
ApplicationTaskExecutorAsyncConfigurer applicationTaskExecutorAsyncConfigurer(BeanFactory beanFactory) {
194+
return new ApplicationTaskExecutorAsyncConfigurer(beanFactory, null);
195+
}
196+
197+
}
198+
181199
@Configuration(proxyBeanMethods = false)
182200
static class BootstrapExecutorConfiguration {
183201

@@ -215,4 +233,39 @@ private static final class ModelCondition {
215233

216234
}
217235

236+
/**
237+
* {@link AsyncConfigurer} implementation that delegates to the user-defined
238+
* {@link AsyncConfigurer} instance, if any. Consistently use the executor named
239+
* {@value TaskExecutionAutoConfiguration#APPLICATION_TASK_EXECUTOR_BEAN_NAME} in the
240+
* absence of a custom executor.
241+
*/
242+
static class ApplicationTaskExecutorAsyncConfigurer implements AsyncConfigurer {
243+
244+
private final BeanFactory beanFactory;
245+
246+
private final @Nullable AsyncConfigurer delegate;
247+
248+
ApplicationTaskExecutorAsyncConfigurer(BeanFactory beanFactory, @Nullable AsyncConfigurer delegate) {
249+
this.beanFactory = beanFactory;
250+
this.delegate = delegate;
251+
}
252+
253+
@Override
254+
public Executor getAsyncExecutor() {
255+
Executor executor = (this.delegate != null) ? this.delegate.getAsyncExecutor() : null;
256+
return (executor != null) ? executor : getApplicationTaskExecutor();
257+
}
258+
259+
@Override
260+
public @Nullable AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
261+
return (this.delegate != null) ? this.delegate.getAsyncUncaughtExceptionHandler() : null;
262+
}
263+
264+
private Executor getApplicationTaskExecutor() {
265+
return this.beanFactory.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
266+
Executor.class);
267+
}
268+
269+
}
270+
218271
}

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
import java.util.function.Consumer;
2626

2727
import org.assertj.core.api.InstanceOfAssertFactories;
28+
import org.jspecify.annotations.Nullable;
2829
import org.junit.jupiter.api.Test;
2930
import org.junit.jupiter.api.condition.EnabledForJreRange;
3031
import org.junit.jupiter.api.condition.JRE;
3132
import org.junit.jupiter.api.extension.ExtendWith;
3233

34+
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
3335
import org.springframework.beans.factory.config.BeanDefinition;
3436
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
3537
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
@@ -48,13 +50,16 @@
4850
import org.springframework.core.task.TaskDecorator;
4951
import org.springframework.core.task.TaskExecutor;
5052
import org.springframework.core.task.support.CompositeTaskDecorator;
53+
import org.springframework.scheduling.TaskScheduler;
5154
import org.springframework.scheduling.annotation.Async;
5255
import org.springframework.scheduling.annotation.AsyncConfigurer;
5356
import org.springframework.scheduling.annotation.EnableAsync;
5457
import org.springframework.scheduling.annotation.EnableScheduling;
5558
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
59+
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
5660

5761
import static org.assertj.core.api.Assertions.assertThat;
62+
import static org.mockito.Mockito.mock;
5863

5964
/**
6065
* Tests for {@link TaskExecutionAutoConfiguration}.
@@ -390,6 +395,62 @@ public Executor getAsyncExecutor() {
390395
});
391396
}
392397

398+
@Test
399+
void enableAsyncLinksToCustomTaskExecutorWhenAsyncConfigurerOverridesIt() {
400+
Executor executor = createCustomAsyncExecutor("async-task-");
401+
AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler = mock(AsyncUncaughtExceptionHandler.class);
402+
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
403+
.withBean("taskScheduler", TaskScheduler.class, () -> mock(ThreadPoolTaskScheduler.class))
404+
.withBean("customAsyncConfigurer", AsyncConfigurer.class, () -> new AsyncConfigurer() {
405+
406+
@Override
407+
public @Nullable Executor getAsyncExecutor() {
408+
return executor;
409+
}
410+
411+
@Override
412+
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
413+
return asyncUncaughtExceptionHandler;
414+
}
415+
})
416+
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
417+
.run((context) -> {
418+
assertThat(context).hasSingleBean(AsyncConfigurer.class);
419+
assertThat(context.getBeansOfType(Executor.class)).containsOnlyKeys("applicationTaskExecutor",
420+
"taskScheduler");
421+
TestBean bean = context.getBean(TestBean.class);
422+
String text = bean.echo("something").get();
423+
assertThat(text).contains("async-task-").contains("something");
424+
AsyncConfigurer asyncConfigurer = context.getBean(AsyncConfigurer.class);
425+
assertThat(asyncConfigurer.getAsyncExecutor()).isEqualTo(executor);
426+
assertThat(asyncConfigurer.getAsyncUncaughtExceptionHandler()).isEqualTo(asyncUncaughtExceptionHandler);
427+
});
428+
}
429+
430+
@Test
431+
void enableAsyncLinksToApplicationTaskExecutorWhenAsyncConfigurerDoesNotOverrideIt() {
432+
AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler = mock(AsyncUncaughtExceptionHandler.class);
433+
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
434+
.withBean("taskScheduler", TaskScheduler.class, () -> mock(ThreadPoolTaskScheduler.class))
435+
.withBean("customAsyncConfigurer", AsyncConfigurer.class, () -> new AsyncConfigurer() {
436+
@Override
437+
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
438+
return asyncUncaughtExceptionHandler;
439+
}
440+
})
441+
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
442+
.run((context) -> {
443+
assertThat(context).hasSingleBean(AsyncConfigurer.class);
444+
assertThat(context.getBeansOfType(Executor.class)).containsOnlyKeys("applicationTaskExecutor",
445+
"taskScheduler");
446+
TestBean bean = context.getBean(TestBean.class);
447+
String text = bean.echo("something").get();
448+
assertThat(text).contains("auto-task-").contains("something");
449+
assertThat(context.getBean(AsyncConfigurer.class).getAsyncUncaughtExceptionHandler())
450+
.isEqualTo(asyncUncaughtExceptionHandler);
451+
});
452+
}
453+
393454
@Test
394455
void enableAsyncUsesAutoConfiguredExecutorWhenModeIsForceAndHasPrimaryCustomTaskExecutor() {
395456
this.contextRunner

0 commit comments

Comments
 (0)