Spring Cloud Managed Thread Pools Are Automatically Wrapped with Trace Information to Preserve Context

This article explains how Spring Cloud automatically wraps container‑managed thread pool beans with tracing proxies to preserve distributed‑trace information, details the ExecutorBeanPostProcessor implementation, shows the relevant code for detecting and instrumenting executors, and notes when manual wrapping is required.

Cognitive Technology Team
Cognitive Technology Team
Cognitive Technology Team
Spring Cloud Managed Thread Pools Are Automatically Wrapped with Trace Information to Preserve Context

Spring Cloud container‑managed thread pool instances are automatically wrapped with tracing proxies so that distributed‑trace (link) information is retained across asynchronous executions.

The core of this functionality is the

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SleuthAsyncProperties.class)
@ConditionalOnProperty(value = "spring.sleuth.async.enabled", matchIfMissing = true)
@ConditionalOnBean(Tracer.class)
@AutoConfigureAfter(BraveAutoConfiguration.class)
public class TraceAsyncDefaultAutoConfiguration {
    @Bean
    @ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true)
    static ExecutorBeanPostProcessor executorBeanPostProcessor(BeanFactory beanFactory) {
        return new ExecutorBeanPostProcessor(beanFactory);
    }
}

implements BeanPostProcessor and intercepts bean creation.

In postProcessAfterInitialization, the processor checks whether the bean is an Executor and not already a tracing wrapper. The check is performed by ExecutorInstrumentor.isApplicableForInstrumentation:

public static boolean isApplicableForInstrumentation(Object bean) {
    return bean instanceof Executor && !(bean instanceof LazyTraceThreadPoolTaskExecutor
        || bean instanceof TraceableScheduledExecutorService || bean instanceof TraceableExecutorService
        || bean instanceof LazyTraceAsyncTaskExecutor || bean instanceof LazyTraceExecutor);
}

If the bean qualifies, the instrument method of ExecutorInstrumentor creates a proxy appropriate to the concrete executor type:

public Object instrument(Object bean, String beanName) {
    if (!isApplicableForInstrumentation(bean)) {
        log.info("Bean is already instrumented or is not applicable for instrumentation " + beanName);
        return bean;
    }
    if (bean instanceof ThreadPoolTaskExecutor) {
        if (isProxyNeeded(beanName)) {
            return wrapThreadPoolTaskExecutor(bean, beanName);
        } else {
            log.info("Not instrumenting bean " + beanName);
        }
    } else if (bean instanceof ScheduledExecutorService) {
        if (isProxyNeeded(beanName)) {
            return wrapScheduledExecutorService(bean, beanName);
        } else {
            log.info("Not instrumenting bean " + beanName);
        }
    } else if (bean instanceof ExecutorService) {
        if (isProxyNeeded(beanName)) {
            return wrapExecutorService(bean, beanName);
        } else {
            log.info("Not instrumenting bean " + beanName);
        }
    } else if (bean instanceof AsyncTaskExecutor) {
        if (isProxyNeeded(beanName)) {
            return wrapAsyncTaskExecutor(bean, beanName);
        } else {
            log.info("Not instrumenting bean " + beanName);
        }
    } else if (bean instanceof Executor) {
        return wrapExecutor(bean, beanName);
    }
    return bean;
}

private Object wrapExecutor(Object bean, String beanName) {
    Executor executor = (Executor) bean;
    boolean methodFinal = anyFinalMethods(executor);
    boolean classFinal = Modifier.isFinal(bean.getClass().getModifiers());
    boolean cglibProxy = !methodFinal && !classFinal;
    try {
        return createProxy(bean, cglibProxy, new ExecutorMethodInterceptor<>(executor, this.beanFactory, beanName));
    } catch (AopConfigException ex) {
        if (cglibProxy) {
            if (log.isDebugEnabled()) {
                log.debug("Exception occurred while trying to create a proxy, falling back to JDK proxy", ex);
            }
            return createProxy(bean, false, new ExecutorMethodInterceptor<>(executor, this.beanFactory, beanName));
        }
        throw ex;
    }
}

The proxy creation respects whether the original executor class is final; if it is, a byte‑code (CGLIB) proxy is used, otherwise a JDK dynamic proxy may be applied.

The implementation comes from the spring-cloud-sleuth-autoconfigure module version 3.1.1. For thread pools that are manually instantiated (e.g., via new or not declared as Spring beans), developers must explicitly wrap them with a tracing executor to retain link information.

Original Source

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

Sign in to view source
Republication Notice

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

JavaSpring Bootthread poolSpring Cloudtracing
Cognitive Technology Team
Written by

Cognitive Technology Team

Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

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