Spring Task Scheduling & Async Execution: Configure Thread Pools Like a Pro

This article explains how Spring 5.3’s @Scheduled and @Async annotations are processed, detailing the bean lookup sequence for TaskScheduler and AsyncConfigurer, the default thread pool creation mechanisms, and the configuration of web asynchronous support, with code examples for each step.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Task Scheduling & Async Execution: Configure Thread Pools Like a Pro

Environment

Spring 5.3.23

3.1 Task Scheduling

Annotation class: @Scheduled Core processing class: ScheduledAnnotationBeanPostProcessor Thread pool used: TaskScheduler (looked up from the container)

First, look for a TaskScheduler bean by type; if none is found, throw NoSuchBeanDefinitionException.

If multiple beans are found, search for a bean named taskScheduler.

If still not found, look for a bean of type java.util.concurrent.ScheduledExecutorService.

If none is found, create a default scheduler:

this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);

Spring Boot provides an auto‑configuration class TaskSchedulingAutoConfiguration that defines a TaskScheduler bean using TaskSchedulerBuilder:

public class TaskSchedulingAutoConfiguration {
    @Bean
    @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
    public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
        return builder.build();
    }

    @Bean
    @ConditionalOnMissingBean
    public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
            ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
        TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
        builder.poolSize(properties.getPool().getSize());
        Shutdown shutdown = properties.getShutdown();
        builder.awaitTermination(shutdown.isAwaitTermination());
        builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
        builder.threadNamePrefix(properties.getThreadNamePrefix());
        builder.customizers(taskSchedulerCustomizers);
        return builder;
    }
}

3.2 Asynchronous Tasks

Annotation class: @Async Core processing class: AsyncAnnotationBeanPostProcessor Configuration is performed by ProxyAsyncConfiguration, which extends AbstractAsyncConfiguration. The abstract class defines two suppliers:

protected Supplier<Executor> executor;
protected Supplier<AsyncUncaughtExceptionHandler> exceptionHandler;

It also contains a method to set the single AsyncConfigurer bean found in the container and adapt its getAsyncExecutor and getAsyncUncaughtExceptionHandler methods.

Thread‑pool lookup process:

Search the container for a bean of type AsyncConfigurer.

If none is found, the default executor is obtained from AsyncConfigurer::getAsyncExecutor, which returns null by default.

If the container still provides no executor, AsyncAnnotationBeanPostProcessor ends up with a null executor.

During initialization, AsyncAnnotationBeanPostProcessor creates an AsyncAnnotationAdvisor that holds the executor and exception handler.

Key code fragments:

public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
    @Nullable
    private Supplier<Executor> executor;
    @Nullable
    private Supplier<AsyncUncaughtExceptionHandler> exceptionHandler;
    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        super.setBeanFactory(beanFactory);
        AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
        if (this.asyncAnnotationType != null) {
            advisor.setAsyncAnnotationType(this.asyncAnnotationType);
        }
        advisor.setBeanFactory(beanFactory);
        this.advisor = advisor;
    }
}

public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
    public AsyncAnnotationAdvisor(@Nullable Supplier<Executor> executor,
            @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
        Set<Class<? extends Annotation>> asyncAnnotationTypes = new LinkedHashSet<>(2);
        asyncAnnotationTypes.add(Async.class);
        try {
            asyncAnnotationTypes.add((Class<? extends Annotation>)
                ClassUtils.forName("javax.ejb.Asynchronous", AsyncAnnotationAdvisor.class.getClassLoader()));
        } catch (Throwable ex) {
            // ignore
        }
        this.advice = buildAdvice(executor, exceptionHandler);
        this.pointcut = buildPointcut(asyncAnnotationTypes);
    }
    // ...
}

public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware {
    public void configure(@Nullable Supplier<Executor> defaultExecutor,
            @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
        this.defaultExecutor = new SingletonSupplier<>(defaultExecutor,
            () -> getDefaultExecutor(this.beanFactory));
        this.exceptionHandler = new SingletonSupplier<>(exceptionHandler, SimpleAsyncUncaughtExceptionHandler::new);
    }
    // ...
}

public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered {
    @Override
    protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
        Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
        return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
    }
}

Summary of the lookup:

First, look for a TaskExecutor bean in the container.

If not found, search for a bean named taskExecutor of type java.util.concurrent.Executor.

If still missing, Spring creates a default SimpleAsyncTaskExecutor, an unbounded thread pool that spawns a new thread per task.

3.3 Web Asynchronous Interface

The core class is RequestMappingHandlerAdapter, which by default uses an unbounded SimpleAsyncTaskExecutor:

public class RequestMappingHandlerAdapter {
    // default is an unbounded thread pool
    private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.setTaskExecutor(this.taskExecutor);
        // ...
    }
}

Bean creation and configuration flow:

public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {}

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
    @Override
    protected void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        this.configurers.configureAsyncSupport(configurer);
    }
}

public class WebMvcAutoConfiguration {
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            if (this.beanFactory.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
                Object taskExecutor = this.beanFactory.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
                if (taskExecutor instanceof AsyncTaskExecutor) {
                    configurer.setTaskExecutor((AsyncTaskExecutor) taskExecutor);
                }
            }
            Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
            if (timeout != null) {
                configurer.setDefaultTimeout(timeout.toMillis());
            }
        }
    }
}

Summary:

By default, Spring uses SimpleAsyncTaskExecutor (unbounded).

If a bean named applicationTaskExecutor of type AsyncTaskExecutor exists, it is used instead.

Understanding these three thread‑pool mechanisms enables correct configuration and usage of task scheduling, asynchronous method execution, and web async support in Spring applications.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javatask schedulingspringthread poolAsync
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.