Automatic Trace-Wrapped ThreadPool Instances in Spring Cloud
This article explains how Spring Cloud automatically wraps managed thread pool beans with trace-enabled proxies to preserve distributed tracing information, details the ExecutorBeanPostProcessor implementation, shows the relevant configuration and instrumentation code, and notes that manually created executors must be wrapped manually.
Spring Cloud-managed thread pool instances are automatically wrapped with trace information to prevent loss of distributed tracing context. The article introduces the problem of trace loss in thread pools and demonstrates how Spring Cloud’s container‑managed executors are instrumented.
ExecutorBeanPostProcessor proxies the original thread pool
1. ExecutorBeanPostProcessor implements BeanPostProcessor
@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);
}
}2. In postProcessAfterInitialization , the processor checks whether the bean is an Executor and not already trace‑wrapped; if so, it creates a trace‑enabled proxy.
/**
* Bean post processor that wraps a call to an {@link Executor} either in a JDK or CGLIB
* proxy. Depending on whether the implementation has a final method or is final.
*/
public class ExecutorBeanPostProcessor implements BeanPostProcessor {
private static final Log log = LogFactory.getLog(ExecutorBeanPostProcessor.class);
private final BeanFactory beanFactory;
private SleuthAsyncProperties sleuthAsyncProperties;
public ExecutorBeanPostProcessor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!ExecutorInstrumentor.isApplicableForInstrumentation(bean)) {
return bean;
}
return new ExecutorInstrumentor(() -> sleuthAsyncProperties().getIgnoredBeans(), this.beanFactory)
.instrument(bean, beanName);
}
private SleuthAsyncProperties sleuthAsyncProperties() {
if (this.sleuthAsyncProperties == null) {
this.sleuthAsyncProperties = this.beanFactory.getBean(SleuthAsyncProperties.class);
}
return this.sleuthAsyncProperties;
}
}The method ExecutorInstrumentor.isApplicableForInstrumentation determines if a bean needs tracing:
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);
}3. Creating the trace‑wrapped proxy object
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;
}
}Depending on the concrete thread‑pool type, the processor creates either a JDK dynamic proxy or a CGLIB proxy; if the original class is final, only a byte‑code‑based proxy can be used.
The source of the article is the spring-cloud-sleuth-autoconfigure module, version 3.1.1.
Conclusion
Spring Cloud automatically wraps container‑managed thread‑pool beans with trace‑enabled proxies, ensuring that distributed tracing information is propagated. For thread pools instantiated manually (e.g., via new or not registered as beans), developers must explicitly wrap them to retain trace context.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.