Commit c071f34a authored by Stephane Nicoll's avatar Stephane Nicoll

Add auto-configuration support for TaskExecutor

This commit adds support for providing a default ThreadPoolTaskExecutor
with sensible defaults. A new TaskExecutorBuilder is provided with
defaults from the `spring.task.*` namespace and can be used to create
custom instances.

If no custom `Executor` bean is present, `@EnableAsync` now uses the
auto-configure application task executor. Same goes for the async support
in Spring MVC.

Closes gh-1563
parent 193b2f18
/*
* Copyright 2012-2018 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
*
* http://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.Executor;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link TaskExecutor}.
*
* @author Stephane Nicoll
* @since 2.1.0
*/
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@Configuration
@EnableConfigurationProperties(TaskProperties.class)
public class TaskExecutorAutoConfiguration {
/**
* Bean name of the application {@link TaskExecutor}.
*/
public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";
private final TaskProperties properties;
private final ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers;
private final ObjectProvider<TaskDecorator> taskDecorator;
public TaskExecutorAutoConfiguration(TaskProperties properties,
ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
ObjectProvider<TaskDecorator> taskDecorator) {
this.properties = properties;
this.taskExecutorCustomizers = taskExecutorCustomizers;
this.taskDecorator = taskDecorator;
}
@Bean
@ConditionalOnMissingBean
public TaskExecutorBuilder taskExecutorBuilder() {
TaskExecutorBuilder builder = new TaskExecutorBuilder();
TaskProperties.Pool pool = this.properties.getPool();
builder = builder.queueCapacity(pool.getQueueCapacity())
.corePoolSize(pool.getCoreSize()).maxPoolSize(pool.getMaxSize())
.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout())
.keepAlive(pool.getKeepAlive());
builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix());
builder = builder.customizers(
this.taskExecutorCustomizers.stream().collect(Collectors.toList()));
TaskDecorator taskDecorator = this.taskDecorator.getIfUnique();
if (taskDecorator != null) {
builder = builder.taskDecorator(taskDecorator);
}
return builder;
}
@Bean(name = APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@ConditionalOnMissingBean(Executor.class)
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
}
}
/*
* Copyright 2012-2018 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
*
* http://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.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for task execution.
*
* @author Stephane Nicoll
*/
@ConfigurationProperties("spring.task")
public class TaskProperties {
private final Pool pool = new Pool();
/**
* Prefix to use for the names of newly created threads.
*/
private String threadNamePrefix = "executor-";
public Pool getPool() {
return this.pool;
}
public String getThreadNamePrefix() {
return this.threadNamePrefix;
}
public void setThreadNamePrefix(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
public static class Pool {
/**
* Queue capacity. A unbounded capacity does not increase the pool and therefore
* ignores the "max-size" parameter.
*/
private int queueCapacity = Integer.MAX_VALUE;
/**
* Core number of threads.
*/
private int coreSize = 8;
/**
* Maximum allowed number of threads. If tasks are filling up the queue, the pool
* can expand up to that size to accommodate the load. Ignored if the queue is
* unbounded.
*/
private int maxSize = Integer.MAX_VALUE;
/**
* Whether core threads are allowed to time out. This enables dynamic growing and
* shrinking of the pool.
*/
private boolean allowCoreThreadTimeout = true;
/**
* Time limit for which threads may remain idle before being terminated.
*/
private Duration keepAlive = Duration.ofSeconds(60);
public int getQueueCapacity() {
return this.queueCapacity;
}
public void setQueueCapacity(int queueCapacity) {
this.queueCapacity = queueCapacity;
}
public int getCoreSize() {
return this.coreSize;
}
public void setCoreSize(int coreSize) {
this.coreSize = coreSize;
}
public int getMaxSize() {
return this.maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public boolean isAllowCoreThreadTimeout() {
return this.allowCoreThreadTimeout;
}
public void setAllowCoreThreadTimeout(boolean allowCoreThreadTimeout) {
this.allowCoreThreadTimeout = allowCoreThreadTimeout;
}
public Duration getKeepAlive() {
return this.keepAlive;
}
public void setKeepAlive(Duration keepAlive) {
this.keepAlive = keepAlive;
}
}
}
/*
* Copyright 2012-2018 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
*
* http://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.
*/
/**
* Auto-configuration for task execution.
*/
package org.springframework.boot.autoconfigure.task;
...@@ -46,6 +46,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; ...@@ -46,6 +46,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.task.TaskExecutorAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
...@@ -70,6 +71,7 @@ import org.springframework.core.convert.converter.GenericConverter; ...@@ -70,6 +71,7 @@ import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
...@@ -140,7 +142,7 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; ...@@ -140,7 +142,7 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver;
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class }) TaskExecutorAutoConfiguration.class, ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration { public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = ""; public static final String DEFAULT_PREFIX = "";
...@@ -210,6 +212,14 @@ public class WebMvcAutoConfiguration { ...@@ -210,6 +212,14 @@ public class WebMvcAutoConfiguration {
@Override @Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) { public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
if (this.beanFactory.containsBean(
TaskExecutorAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
Object taskExecutor = this.beanFactory.getBean(
TaskExecutorAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
if (taskExecutor instanceof AsyncTaskExecutor) {
configurer.setTaskExecutor(((AsyncTaskExecutor) taskExecutor));
}
}
Duration timeout = this.mvcProperties.getAsync().getRequestTimeout(); Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
if (timeout != null) { if (timeout != null) {
configurer.setDefaultTimeout(timeout.toMillis()); configurer.setDefaultTimeout(timeout.toMillis());
......
...@@ -106,6 +106,7 @@ org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\ ...@@ -106,6 +106,7 @@ org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\ org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutorAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
......
/*
* Copyright 2012-2018 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
*
* http://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.Executor;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link TaskExecutorAutoConfiguration}.
*
* @author Stephane Nicoll
*/
public class TaskExecutorAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(TaskExecutorAutoConfiguration.class));
@Test
public void taskExecutorBuilderShouldApplyCustomSettings() {
this.contextRunner
.withPropertyValues("spring.task.pool.queue-capacity=10",
"spring.task.pool.core-size=2", "spring.task.pool.max-size=4",
"spring.task.pool.allow-core-thread-timeout=true",
"spring.task.pool.keep-alive=5s",
"spring.task.thread-name-prefix=mytest-")
.run(assertTaskExecutor((taskExecutor) -> {
DirectFieldAccessor dfa = new DirectFieldAccessor(taskExecutor);
assertThat(dfa.getPropertyValue("queueCapacity")).isEqualTo(10);
assertThat(taskExecutor.getCorePoolSize()).isEqualTo(2);
assertThat(taskExecutor.getMaxPoolSize()).isEqualTo(4);
assertThat(dfa.getPropertyValue("allowCoreThreadTimeOut"))
.isEqualTo(true);
assertThat(taskExecutor.getKeepAliveSeconds()).isEqualTo(5);
assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-");
}));
}
@Test
public void taskExecutorBuilderWhenHasCustomBuilderShouldUseCustomBuilder() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorBuilderConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(TaskExecutorBuilder.class);
assertThat(context.getBean(TaskExecutorBuilder.class))
.isSameAs(context.getBean(
CustomTaskExecutorBuilderConfig.class).taskExecutorBuilder);
});
}
@Test
public void taskExecutorBuilderShouldUseTaskDecorator() {
this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(TaskExecutorBuilder.class);
ThreadPoolTaskExecutor executor = context
.getBean(TaskExecutorBuilder.class).build();
assertThat(ReflectionTestUtils.getField(executor, "taskDecorator"))
.isSameAs(context.getBean(TaskDecorator.class));
});
}
@Test
public void taskExecutorAutoConfigured() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(Executor.class);
assertThat(context).hasBean("applicationTaskExecutor");
assertThat(context).getBean("applicationTaskExecutor")
.isInstanceOf(ThreadPoolTaskExecutor.class);
});
}
@Test
public void taskExecutorWhenHasCustomTaskExecutorShouldBAckOff() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(Executor.class);
assertThat(context.getBean(Executor.class))
.isSameAs(context.getBean("customTaskExecutorBuilder"));
});
}
@Test
public void taskExecutorBuilderShouldApplyCustomizer() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class,
TaskExecutorCustomizerConfig.class).run((context) -> {
TaskExecutorCustomizer customizer = context
.getBean(TaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = context
.getBean(TaskExecutorBuilder.class).build();
verify(customizer).customize(executor);
});
}
@Test
public void enableAsyncUsesAutoConfiguredOneByDefault() {
this.contextRunner
.withPropertyValues("spring.task.thread-name-prefix=executor-test-")
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).hasSingleBean(TaskExecutor.class);
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("test").get();
assertThat(text).contains("executor-test-").contains("test");
});
}
private ContextConsumer<AssertableApplicationContext> assertTaskExecutor(
Consumer<ThreadPoolTaskExecutor> taskExecutor) {
return (context) -> {
assertThat(context).hasSingleBean(TaskExecutorBuilder.class);
TaskExecutorBuilder builder = context.getBean(TaskExecutorBuilder.class);
taskExecutor.accept(builder.build());
};
}
@Configuration
static class CustomTaskExecutorBuilderConfig {
private final TaskExecutorBuilder taskExecutorBuilder = new TaskExecutorBuilder();
@Bean
public TaskExecutorBuilder customTaskExecutorBuilder() {
return this.taskExecutorBuilder;
}
}
@Configuration
static class TaskExecutorCustomizerConfig {
@Bean
public TaskExecutorCustomizer mockTaskExecutorCustomizer() {
return mock(TaskExecutorCustomizer.class);
}
}
@Configuration
static class TaskDecoratorConfig {
@Bean
public TaskDecorator mockTaskDecorator() {
return mock(TaskDecorator.class);
}
}
@Configuration
static class CustomTaskExecutorConfig {
@Bean
public Executor customTaskExecutorBuilder() {
return new SyncTaskExecutor();
}
}
@Configuration
@EnableAsync
static class AsyncConfiguration {
}
static class TestBean {
@Async
public Future<String> echo(String text) {
return new AsyncResult<>(Thread.currentThread().getName() + " " + text);
}
}
}
...@@ -22,6 +22,7 @@ import java.util.LinkedHashMap; ...@@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
...@@ -36,6 +37,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; ...@@ -36,6 +37,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutorAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter;
...@@ -52,6 +54,7 @@ import org.springframework.context.annotation.Import; ...@@ -52,6 +54,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
...@@ -75,6 +78,7 @@ import org.springframework.web.servlet.HandlerMapping; ...@@ -75,6 +78,7 @@ import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.View; import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
...@@ -471,6 +475,63 @@ public class WebMvcAutoConfigurationTests { ...@@ -471,6 +475,63 @@ public class WebMvcAutoConfigurationTests {
"asyncRequestTimeout")).isEqualTo(12345L)); "asyncRequestTimeout")).isEqualTo(12345L));
} }
@Test
public void asyncTaskExecutorWithApplicationTaskExecutor() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(TaskExecutorAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(AsyncTaskExecutor.class);
assertThat(ReflectionTestUtils.getField(
context.getBean(RequestMappingHandlerAdapter.class),
"taskExecutor"))
.isSameAs(context.getBean("applicationTaskExecutor"));
});
}
@Test
public void asyncTaskExecutorWithNonMatchApplicationTaskExecutorBean() {
this.contextRunner
.withUserConfiguration(CustomApplicationTaskExecutorConfig.class)
.withConfiguration(
AutoConfigurations.of(TaskExecutorAutoConfiguration.class))
.run((context) -> {
assertThat(context).doesNotHaveBean(AsyncTaskExecutor.class);
assertThat(ReflectionTestUtils.getField(
context.getBean(RequestMappingHandlerAdapter.class),
"taskExecutor")).isNotSameAs(
context.getBean("applicationTaskExecutor"));
});
}
@Test
public void asyncTaskExecutorWithMvcConfigurerCanOverrideExecutor() {
this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfigurer.class)
.withConfiguration(
AutoConfigurations.of(TaskExecutorAutoConfiguration.class))
.run((context) -> {
assertThat(ReflectionTestUtils.getField(
context.getBean(RequestMappingHandlerAdapter.class),
"taskExecutor"))
.isSameAs(context.getBean(
CustomAsyncTaskExecutorConfigurer.class).taskExecutor);
});
}
@Test
public void asyncTaskExecutorWithCustomNonApplicationTaskExecutor() {
this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfig.class)
.withConfiguration(
AutoConfigurations.of(TaskExecutorAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(AsyncTaskExecutor.class);
assertThat(ReflectionTestUtils.getField(
context.getBean(RequestMappingHandlerAdapter.class),
"taskExecutor"))
.isNotSameAs(context.getBean("customTaskExecutor"));
});
}
@Test @Test
public void customMediaTypes() { public void customMediaTypes() {
this.contextRunner this.contextRunner
...@@ -1124,4 +1185,36 @@ public class WebMvcAutoConfigurationTests { ...@@ -1124,4 +1185,36 @@ public class WebMvcAutoConfigurationTests {
} }
@Configuration
static class CustomApplicationTaskExecutorConfig {
@Bean
public Executor applicationTaskExecutor() {
return mock(Executor.class);
}
}
@Configuration
static class CustomAsyncTaskExecutorConfig {
@Bean
public AsyncTaskExecutor customTaskExecutor() {
return mock(AsyncTaskExecutor.class);
}
}
@Configuration
static class CustomAsyncTaskExecutorConfigurer implements WebMvcConfigurer {
private final AsyncTaskExecutor taskExecutor = mock(AsyncTaskExecutor.class);
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setTaskExecutor(this.taskExecutor);
}
}
} }
...@@ -160,6 +160,13 @@ content into your application. Rather, pick only the properties that you need. ...@@ -160,6 +160,13 @@ content into your application. Rather, pick only the properties that you need.
spring.sendgrid.proxy.host= # SendGrid proxy host. spring.sendgrid.proxy.host= # SendGrid proxy host.
spring.sendgrid.proxy.port= # SendGrid proxy port. spring.sendgrid.proxy.port= # SendGrid proxy port.
# TASK EXECUTION ({sc-spring-boot-autoconfigure}/task/TaskProperties.{sc-ext}[TaskProperties])
spring.task.pool.allow-core-thread-timeout=true # Whether core threads are allowed to time out. This enables dynamic growing and shrinking of the pool.
spring.task.pool.core-size=8 # Core number of threads.
spring.task.pool.keep-alive=60s # Time limit for which threads may remain idle before being terminated.
spring.task.pool.max-size= # Maximum allowed number of threads. If tasks are filling up the queue, the pool can expand up to that size to accommodate the load. Ignored if the queue is unbounded.
spring.task.pool.queue-capacity= # Queue capacity. A unbounded capacity does not increase the pool and therefore ignores the "max-size" parameter.
spring.task.thread-name-prefix=executor- # Prefix to use for the names of newly created threads.
# ---------------------------------------- # ----------------------------------------
# WEB PROPERTIES # WEB PROPERTIES
......
...@@ -6080,6 +6080,32 @@ in a similar manner, as shown in the following example: ...@@ -6080,6 +6080,32 @@ in a similar manner, as shown in the following example:
[[boot-features-task-execution]]
== Task Execution
In the absence of a `TaskExecutor` bean in the context, Spring Boot auto-configures a
`ThreadPoolTaskExecutor` with sensible defaults that can be automatically associated to
asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request
processing.
The thread pool uses 8 core threads that can grow and shrink according to the load. Those
default settings can be fine-tuned using the `spring.task` namespace as shown in the
following example:
[source,properties,indent=0]
----
spring.task.pool.max-threads=16
spring.task.pool.queue-capacity=100
spring.task.pool.keep-alive=10s
----
This changes the thread pool to use a bounded queue so that when the queue is full (100
tasks), the thread pool increases to maximum 16 threads. Shrinking of the pool is more
aggressive as well as threads are reclaimed when they are idle for 10 seconds (rather than
60 seconds by default).
[[boot-features-integration]] [[boot-features-integration]]
== Spring Integration == Spring Integration
Spring Boot offers several conveniences for working with {spring-integration}[Spring Spring Boot offers several conveniences for working with {spring-integration}[Spring
......
/*
* Copyright 2012-2018 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
*
* http://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.ThreadPoolTaskExecutor;
/**
* Callback interface that can be used to customize a {@link ThreadPoolTaskExecutor}.
*
* @author Stephane Nicoll
* @since 2.1.0
* @see TaskExecutorBuilder
*/
@FunctionalInterface
public interface TaskExecutorCustomizer {
/**
* Callback to customize a {@link ThreadPoolTaskExecutor} instance.
* @param taskExecutor the task executor to customize
*/
void customize(ThreadPoolTaskExecutor taskExecutor);
}
/*
* Copyright 2012-2018 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
*
* http://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.
*/
/**
* Task execution utilities.
*
* @author Stephane Nicoll
*/
package org.springframework.boot.task;
/*
* Copyright 2012-2018 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
*
* http://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.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link TaskExecutorBuilder}.
*
* @author Stephane Nicoll
*/
public class TaskExecutorBuilderTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private TaskExecutorBuilder builder = new TaskExecutorBuilder();
@Test
public void createWhenCustomizersAreNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TaskExecutorCustomizers must not be null");
new TaskExecutorBuilder((TaskExecutorCustomizer[]) null);
}
@Test
public void poolSettingsShouldApply() {
ThreadPoolTaskExecutor executor = this.builder.allowCoreThreadTimeOut(true)
.queueCapacity(10).corePoolSize(4).maxPoolSize(8)
.allowCoreThreadTimeOut(true).keepAlive(Duration.ofMinutes(1)).build();
DirectFieldAccessor dfa = new DirectFieldAccessor(executor);
assertThat(dfa.getPropertyValue("queueCapacity")).isEqualTo(10);
assertThat(executor.getCorePoolSize()).isEqualTo(4);
assertThat(executor.getMaxPoolSize()).isEqualTo(8);
assertThat(dfa.getPropertyValue("allowCoreThreadTimeOut")).isEqualTo(true);
assertThat(executor.getKeepAliveSeconds()).isEqualTo(60);
}
@Test
public void threadNamePrefixShouldApply() {
ThreadPoolTaskExecutor executor = this.builder.threadNamePrefix("test-").build();
assertThat(executor.getThreadNamePrefix()).isEqualTo("test-");
}
@Test
public void taskDecoratorShouldApply() {
TaskDecorator taskDecorator = mock(TaskDecorator.class);
ThreadPoolTaskExecutor executor = this.builder.taskDecorator(taskDecorator)
.build();
assertThat(ReflectionTestUtils.getField(executor, "taskDecorator"))
.isSameAs(taskDecorator);
}
@Test
public void customizersWhenCustomizersAreNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TaskExecutorCustomizers must not be null");
this.builder.customizers((TaskExecutorCustomizer[]) null);
}
@Test
public void customizersCollectionWhenCustomizersAreNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TaskExecutorCustomizers must not be null");
this.builder.customizers((Set<TaskExecutorCustomizer>) null);
}
@Test
public void customizersShouldApply() {
TaskExecutorCustomizer customizer = mock(TaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = this.builder.customizers(customizer).build();
verify(customizer).customize(executor);
}
@Test
public void customizersShouldBeAppliedLast() {
TaskDecorator taskDecorator = mock(TaskDecorator.class);
ThreadPoolTaskExecutor executor = spy(new ThreadPoolTaskExecutor());
this.builder.allowCoreThreadTimeOut(true).queueCapacity(10).corePoolSize(4)
.maxPoolSize(8).allowCoreThreadTimeOut(true)
.keepAlive(Duration.ofMinutes(1)).threadNamePrefix("test-")
.taskDecorator(taskDecorator).additionalCustomizers((taskExecutor) -> {
verify(taskExecutor).setQueueCapacity(10);
verify(taskExecutor).setCorePoolSize(4);
verify(taskExecutor).setMaxPoolSize(8);
verify(taskExecutor).setAllowCoreThreadTimeOut(true);
verify(taskExecutor).setKeepAliveSeconds(60);
verify(taskExecutor).setThreadNamePrefix("test-");
verify(taskExecutor).setTaskDecorator(taskDecorator);
});
this.builder.configure(executor);
}
@Test
public void customizersShouldReplaceExisting() {
TaskExecutorCustomizer customizer1 = mock(TaskExecutorCustomizer.class);
TaskExecutorCustomizer customizer2 = mock(TaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = this.builder.customizers(customizer1)
.customizers(Collections.singleton(customizer2)).build();
verifyZeroInteractions(customizer1);
verify(customizer2).customize(executor);
}
@Test
public void additionalCustomizersWhenCustomizersAreNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TaskExecutorCustomizers must not be null");
this.builder.additionalCustomizers((TaskExecutorCustomizer[]) null);
}
@Test
public void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("TaskExecutorCustomizers must not be null");
this.builder.additionalCustomizers((Set<TaskExecutorCustomizer>) null);
}
@Test
public void additionalCustomizersShouldAddToExisting() {
TaskExecutorCustomizer customizer1 = mock(TaskExecutorCustomizer.class);
TaskExecutorCustomizer customizer2 = mock(TaskExecutorCustomizer.class);
ThreadPoolTaskExecutor executor = this.builder.customizers(customizer1)
.additionalCustomizers(customizer2).build();
verify(customizer1).customize(executor);
verify(customizer2).customize(executor);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment