diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorAutoConfiguration.java new file mode 100644 index 0000000000..b3be0b377e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorAutoConfiguration.java @@ -0,0 +1,90 @@ +/* + * 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 taskExecutorCustomizers; + + private final ObjectProvider taskDecorator; + + public TaskExecutorAutoConfiguration(TaskProperties properties, + ObjectProvider taskExecutorCustomizers, + ObjectProvider 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(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskProperties.java new file mode 100644 index 0000000000..add12aa196 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskProperties.java @@ -0,0 +1,123 @@ +/* + * 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/package-info.java new file mode 100644 index 0000000000..960bf71f3a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/package-info.java @@ -0,0 +1,20 @@ +/* + * 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; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 1ce1bb8c40..92677c0a7c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -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.Type; 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.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; @@ -70,6 +71,7 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; @@ -140,7 +142,7 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, - ValidationAutoConfiguration.class }) + TaskExecutorAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { public static final String DEFAULT_PREFIX = ""; @@ -210,6 +212,14 @@ public class WebMvcAutoConfiguration { @Override 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(); if (timeout != null) { configurer.setDefaultTimeout(timeout.toMillis()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 69b1f0e790..3b49f16c6f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -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.reactive.ReactiveOAuth2ClientAutoConfiguration,\ org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\ +org.springframework.boot.autoconfigure.task.TaskExecutorAutoConfiguration,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutorAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutorAutoConfigurationTests.java new file mode 100644 index 0000000000..93c15cb108 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutorAutoConfigurationTests.java @@ -0,0 +1,212 @@ +/* + * 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 assertTaskExecutor( + Consumer 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 echo(String text) { + return new AsyncResult<>(Thread.currentThread().getName() + " " + text); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 6267606086..63d4cf9daf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -22,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -36,6 +37,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; 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.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter; @@ -52,6 +54,7 @@ import org.springframework.context.annotation.Import; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; @@ -75,6 +78,7 @@ import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.View; 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.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -471,6 +475,63 @@ public class WebMvcAutoConfigurationTests { "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 public void customMediaTypes() { this.contextRunner @@ -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); + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 50add4549b..45a6118717 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -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.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 diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index a4f257a2d9..fb243687d3 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -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]] == Spring Integration Spring Boot offers several conveniences for working with {spring-integration}[Spring diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java new file mode 100644 index 0000000000..5c3c7e7b3e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java @@ -0,0 +1,307 @@ +/* + * 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.Arrays; +import java.util.Collection; +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.TaskDecorator; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link TaskExecutor}. Provides + * convenience methods to set common {@link ThreadPoolTaskExecutor} settings and register + * {@link #taskDecorator(TaskDecorator)}). For advanced configuration, consider using + * {@link TaskExecutorCustomizer}. + *

+ * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link TaskExecutor} is needed. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +public class TaskExecutorBuilder { + + private final Integer queueCapacity; + + private final Integer corePoolSize; + + private final Integer maxPoolSize; + + private final Boolean allowCoreThreadTimeOut; + + private final Duration keepAlive; + + private final String threadNamePrefix; + + private final TaskDecorator taskDecorator; + + private final Set taskExecutorCustomizers; + + public TaskExecutorBuilder(TaskExecutorCustomizer... taskExecutorCustomizers) { + Assert.notNull(taskExecutorCustomizers, + "TaskExecutorCustomizers must not be null"); + this.queueCapacity = null; + this.corePoolSize = null; + this.maxPoolSize = null; + this.allowCoreThreadTimeOut = null; + this.keepAlive = null; + this.threadNamePrefix = null; + this.taskDecorator = null; + this.taskExecutorCustomizers = Collections.unmodifiableSet( + new LinkedHashSet<>(Arrays.asList(taskExecutorCustomizers))); + } + + public TaskExecutorBuilder(Integer queueCapacity, Integer corePoolSize, + Integer maxPoolSize, Boolean allowCoreThreadTimeOut, Duration keepAlive, + String threadNamePrefix, TaskDecorator taskDecorator, + Set taskExecutorCustomizers) { + this.queueCapacity = queueCapacity; + this.corePoolSize = corePoolSize; + this.maxPoolSize = maxPoolSize; + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + this.keepAlive = keepAlive; + this.threadNamePrefix = threadNamePrefix; + this.taskDecorator = taskDecorator; + this.taskExecutorCustomizers = taskExecutorCustomizers; + } + + /** + * Set the capacity of the queue. A unbounded capacity does not increase the pool and + * therefore ignores {@link #maxPoolSize(int) maxPoolSize}. + * @param queueCapacity the queue capacity to set + * @return a new builder instance + */ + public TaskExecutorBuilder queueCapacity(int queueCapacity) { + return new TaskExecutorBuilder(queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.threadNamePrefix, + this.taskDecorator, this.taskExecutorCustomizers); + } + + /** + * Set the core number of threads. Effectively that maximum number of threads as long + * as the queue is not full. + *

+ * Core threads can grow and shrink if {@link #allowCoreThreadTimeOut(boolean)} is + * enabled. + * @param corePoolSize the core pool size to set + * @return a new builder instance + */ + public TaskExecutorBuilder corePoolSize(int corePoolSize) { + return new TaskExecutorBuilder(this.queueCapacity, corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.threadNamePrefix, + this.taskDecorator, this.taskExecutorCustomizers); + } + + /** + * Set the maximum allowed number of threads. When the {@link #queueCapacity(int) + * queue} is full, the pool can expand up to that size to accommodate the load. + *

+ * If the {@link #queueCapacity(int) queue capacity} is unbounded, this setting is + * ignored. + * @param maxPoolSize the max pool size to set + * @return a new builder instance + */ + public TaskExecutorBuilder maxPoolSize(int maxPoolSize) { + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.threadNamePrefix, + this.taskDecorator, this.taskExecutorCustomizers); + } + + /** + * Set whether core threads are allow to time out. When enabled, this enables dynamic + * growing and shrinking of the pool. + * @param allowCoreThreadTimeOut if core thread are allowed to time out + * @return a new builder instance + */ + public TaskExecutorBuilder allowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, + this.maxPoolSize, allowCoreThreadTimeOut, this.keepAlive, + this.threadNamePrefix, this.taskDecorator, this.taskExecutorCustomizers); + } + + /** + * Set the time limit for which threads may remain idle before being terminated. + * @param keepAlive the keep alive to set + * @return a new builder instance + */ + public TaskExecutorBuilder keepAlive(Duration keepAlive) { + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, + this.maxPoolSize, this.allowCoreThreadTimeOut, keepAlive, + this.threadNamePrefix, this.taskDecorator, this.taskExecutorCustomizers); + } + + /** + * 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 TaskExecutorBuilder threadNamePrefix(String threadNamePrefix) { + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, + this.maxPoolSize, this.allowCoreThreadTimeOut, this.keepAlive, + threadNamePrefix, this.taskDecorator, this.taskExecutorCustomizers); + } + + /** + * 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 TaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) { + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, + this.maxPoolSize, this.allowCoreThreadTimeOut, this.keepAlive, + this.threadNamePrefix, taskDecorator, this.taskExecutorCustomizers); + } + + /** + * Set the {@link TaskExecutorCustomizer TaskExecutorCustomizers} that should be + * applied to the {@link ThreadPoolTaskExecutor}. 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 taskExecutorCustomizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(TaskExecutorCustomizer...) + */ + public TaskExecutorBuilder customizers( + TaskExecutorCustomizer... taskExecutorCustomizers) { + Assert.notNull(taskExecutorCustomizers, + "TaskExecutorCustomizers must not be null"); + return customizers(Arrays.asList(taskExecutorCustomizers)); + } + + /** + * Set the {@link TaskExecutorCustomizer TaskExecutorCustomizers} that should be + * applied to the {@link ThreadPoolTaskExecutor}. 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 taskExecutorCustomizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(TaskExecutorCustomizer...) + */ + public TaskExecutorBuilder customizers( + Collection taskExecutorCustomizers) { + Assert.notNull(taskExecutorCustomizers, + "TaskExecutorCustomizers must not be null"); + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, + this.maxPoolSize, this.allowCoreThreadTimeOut, this.keepAlive, + this.threadNamePrefix, this.taskDecorator, + Collections.unmodifiableSet(new LinkedHashSet( + taskExecutorCustomizers))); + } + + /** + * Add {@link TaskExecutorCustomizer TaskExecutorCustomizers} that should be applied + * to the {@link ThreadPoolTaskExecutor}. Customizers are applied in the order that + * they were added after builder configuration has been applied. + * @param taskExecutorCustomizers the customizers to add + * @return a new builder instance + * @see #customizers(TaskExecutorCustomizer...) + */ + public TaskExecutorBuilder additionalCustomizers( + TaskExecutorCustomizer... taskExecutorCustomizers) { + Assert.notNull(taskExecutorCustomizers, + "TaskExecutorCustomizers must not be null"); + return additionalCustomizers(Arrays.asList(taskExecutorCustomizers)); + } + + /** + * Add {@link TaskExecutorCustomizer TaskExecutorCustomizers} that should be applied + * to the {@link ThreadPoolTaskExecutor}. Customizers are applied in the order that + * they were added after builder configuration has been applied. + * @param taskExecutorCustomizers the customizers to add + * @return a new builder instance + * @see #customizers(TaskExecutorCustomizer...) + */ + public TaskExecutorBuilder additionalCustomizers( + Collection taskExecutorCustomizers) { + Assert.notNull(taskExecutorCustomizers, + "TaskExecutorCustomizers must not be null"); + return new TaskExecutorBuilder(this.queueCapacity, this.corePoolSize, + this.maxPoolSize, this.allowCoreThreadTimeOut, this.keepAlive, + this.threadNamePrefix, this.taskDecorator, + append(this.taskExecutorCustomizers, taskExecutorCustomizers)); + } + + /** + * Build a new {@link ThreadPoolTaskExecutor} instance and configure it using this + * builder. + * @return a configured {@link ThreadPoolTaskExecutor} instance. + * @see #build(Class) + * @see #configure(ThreadPoolTaskExecutor) + */ + public ThreadPoolTaskExecutor build() { + return build(ThreadPoolTaskExecutor.class); + } + + /** + * Build a new {@link ThreadPoolTaskExecutor} 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 ThreadPoolTaskExecutor} instance. + * @see TaskExecutorBuilder#build() + * @see #configure(ThreadPoolTaskExecutor) + */ + public T build(Class taskExecutorClass) { + return configure(BeanUtils.instantiateClass(taskExecutorClass)); + } + + /** + * Configure the provided {@link ThreadPoolTaskExecutor} instance using this builder. + * @param the type of task executor + * @param taskExecutor the {@link ThreadPoolTaskExecutor} to configure + * @return the task executor instance + * @see TaskExecutorBuilder#build() + * @see TaskExecutorBuilder#build(Class) + */ + public T configure(T taskExecutor) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(() -> this.queueCapacity).to(taskExecutor::setQueueCapacity); + map.from(() -> this.corePoolSize).to(taskExecutor::setCorePoolSize); + map.from(() -> this.maxPoolSize).to(taskExecutor::setMaxPoolSize); + map.from(() -> this.keepAlive).asInt(Duration::getSeconds) + .to(taskExecutor::setKeepAliveSeconds); + map.from(() -> this.allowCoreThreadTimeOut) + .to(taskExecutor::setAllowCoreThreadTimeOut); + map.from(() -> this.threadNamePrefix).whenHasText() + .to(taskExecutor::setThreadNamePrefix); + map.from(() -> this.taskDecorator).to(taskExecutor::setTaskDecorator); + + if (!CollectionUtils.isEmpty(this.taskExecutorCustomizers)) { + for (TaskExecutorCustomizer customizer : this.taskExecutorCustomizers) { + customizer.customize(taskExecutor); + } + } + return taskExecutor; + } + + private static Set append(Set set, Collection additions) { + Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); + result.addAll(additions); + return Collections.unmodifiableSet(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java new file mode 100644 index 0000000000..df5543333c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java @@ -0,0 +1,37 @@ +/* + * 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); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/package-info.java new file mode 100644 index 0000000000..90205e073a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/package-info.java @@ -0,0 +1,22 @@ +/* + * 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; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java new file mode 100644 index 0000000000..2f560eb81e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java @@ -0,0 +1,159 @@ +/* + * 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) 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) 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); + } + +}