diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml
index 575b993240e..87f3caee62b 100644
--- a/config/checkstyle/import-control.xml
+++ b/config/checkstyle/import-control.xml
@@ -62,7 +62,7 @@
-
+
diff --git a/core/spring-boot-autoconfigure/build.gradle b/core/spring-boot-autoconfigure/build.gradle
index a8686fe2be7..2070dd412b6 100644
--- a/core/spring-boot-autoconfigure/build.gradle
+++ b/core/spring-boot-autoconfigure/build.gradle
@@ -30,6 +30,7 @@ dependencies {
optional("com.github.ben-manes.caffeine:caffeine")
optional("org.aspectj:aspectjweaver")
+ optional("io.micrometer:context-propagation")
optional("jakarta.servlet:jakarta.servlet-api")
optional("javax.money:money-api")
optional("org.springframework:spring-web")
diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java
index ab6eb21cf47..7e17fd524d9 100644
--- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java
+++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java
@@ -35,7 +35,8 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@AutoConfiguration
@EnableConfigurationProperties(TaskExecutionProperties.class)
-@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
+@Import({ TaskExecutorConfigurations.TaskExecutorContextPropagationConfiguration.class,
+ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class,
TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class,
TaskExecutorConfigurations.TaskExecutorConfiguration.class,
TaskExecutorConfigurations.BootstrapExecutorConfiguration.class })
diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java
index 144fbb60e67..e2dbfcab061 100644
--- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java
+++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java
@@ -44,6 +44,11 @@ public class TaskExecutionProperties {
*/
private Mode mode = Mode.AUTO;
+ /**
+ * Whether to propagate the current context to task executions.
+ */
+ private boolean propagateContext;
+
/**
* Prefix to use for the names of newly created threads.
*/
@@ -69,6 +74,14 @@ public class TaskExecutionProperties {
this.mode = mode;
}
+ public boolean getPropagateContext() {
+ return this.propagateContext;
+ }
+
+ public void setPropagateContext(boolean propagateContext) {
+ this.propagateContext = propagateContext;
+ }
+
public String getThreadNamePrefix() {
return this.threadNamePrefix;
}
diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
index a519c65374a..3909065231b 100644
--- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
+++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
@@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.task;
import java.util.List;
import java.util.concurrent.Executor;
+import io.micrometer.context.ContextSnapshot;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
@@ -29,6 +30,7 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
@@ -47,6 +49,7 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
+import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -68,6 +71,18 @@ class TaskExecutorConfigurations {
return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null;
}
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(ContextSnapshot.class)
+ static class TaskExecutorContextPropagationConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(name = "spring.task.execution.propagate-context", havingValue = "true")
+ ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
+ return new ContextPropagatingTaskDecorator();
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
@Conditional(OnExecutorCondition.class)
@Import({ AsyncConfigurerWrapperConfiguration.class, AsyncConfigurerConfiguration.class })
diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
index 40d6fb26e7a..862452952f7 100644
--- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
+++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
@@ -16,6 +16,10 @@
package org.springframework.boot.autoconfigure.task;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -24,6 +28,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import io.micrometer.context.ThreadLocalAccessor;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
@@ -42,6 +47,7 @@ import org.springframework.boot.test.context.assertj.AssertableApplicationContex
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.system.OutputCaptureExtension;
+import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -50,6 +56,7 @@ import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
+import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncConfigurer;
@@ -253,6 +260,35 @@ class TaskExecutionAutoConfigurationTests {
});
}
+ @Test
+ @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
+ content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor")
+ void asyncTaskExecutorShouldNotNotRegisterContextPropagatingTaskDecoratorByDefault() {
+ this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class).run((context) -> {
+ assertThat(context).doesNotHaveBean(ContextPropagatingTaskDecorator.class);
+ TestBean bean = context.getBean(TestBean.class);
+ TestThreadLocalHolder.setValue("from-context");
+ String text = bean.echoContext().get();
+ assertThat(text).contains("task-").endsWith("null");
+ });
+
+ }
+
+ @Test
+ @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
+ content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor")
+ void asyncTaskExecutorWhenContextPropagationIsEnabledShouldRegisterBean() {
+ this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
+ .withPropertyValues("spring.task.execution.propagate-context=true")
+ .run((context) -> {
+ assertThat(context).hasSingleBean(ContextPropagatingTaskDecorator.class);
+ TestBean bean = context.getBean(TestBean.class);
+ TestThreadLocalHolder.setValue("from-context");
+ String text = bean.echoContext().get();
+ assertThat(text).contains("task-").endsWith("from-context");
+ });
+ }
+
@Test
void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> {
@@ -591,6 +627,14 @@ class TaskExecutionAutoConfigurationTests {
return thread.getName();
}
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor",
+ content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests.TestThreadLocalAccessor")
+ @interface WithThreadLocalAccessor {
+
+ }
+
@Configuration(proxyBeanMethods = false)
static class CustomThreadPoolTaskExecutorBuilderConfig {
@@ -622,6 +666,56 @@ class TaskExecutionAutoConfigurationTests {
return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text);
}
+ @Async
+ Future echoContext() {
+ return CompletableFuture
+ .completedFuture(Thread.currentThread().getName() + " " + TestThreadLocalHolder.getValue());
+ }
+
+ }
+
+ static class TestThreadLocalHolder {
+
+ private static final ThreadLocal holder = new ThreadLocal<>();
+
+ static void setValue(String value) {
+ holder.set(value);
+ }
+
+ static String getValue() {
+ return holder.get();
+ }
+
+ static void reset() {
+ holder.remove();
+ }
+
+ }
+
+ public static class TestThreadLocalAccessor implements ThreadLocalAccessor {
+
+ static final String KEY = "test.threadlocal";
+
+ @Override
+ public Object key() {
+ return KEY;
+ }
+
+ @Override
+ public String getValue() {
+ return TestThreadLocalHolder.getValue();
+ }
+
+ @Override
+ public void setValue(String value) {
+ TestThreadLocalHolder.setValue(value);
+ }
+
+ @Override
+ public void setValue() {
+ TestThreadLocalHolder.reset();
+ }
+
}
}
diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc
index e76aeff3c9b..bff522b6396 100644
--- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc
+++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc
@@ -29,8 +29,9 @@ Observability support relies on the https://github.com/micrometer-metrics/contex
By default, javadoc:java.lang.ThreadLocal[] values are not automatically reinstated in reactive operators.
This behavior is controlled with the configprop:spring.reactor.context-propagation[] property, which can be set to `auto` to enable automatic propagation.
-If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods or use an javadoc:org.springframework.core.task.AsyncTaskExecutor[], you have to register the javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] on the executor, otherwise the observability context is lost when switching threads.
-This can be done using this configuration:
+If you're working with javadoc:org.springframework.scheduling.annotation.Async[format=annotation] methods and the javadoc:org.springframework.core.task.AsyncTaskExecutor[] is auto-configured, you have to opt-in for context propagation using the configprop:spring.task.execution.propagate-context[] property.
+
+If you are configuring the javadoc:org.springframework.core.task.AsyncTaskExecutor[] yourself, then you need to register a javadoc:org.springframework.core.task.support.ContextPropagatingTaskDecorator[] bean, as shown in the following example:
include-code::ContextPropagationConfiguration[]