Class FlowContextPropagatingExecutor

java.lang.Object
com.fayupable.logged.spring.aspect.FlowContextPropagatingExecutor
All Implemented Interfaces:
Executor

public final class FlowContextPropagatingExecutor extends Object implements Executor
Executor decorator that carries the submitting thread's FlowContext and MDC context over to whatever thread actually runs the submitted task.

FlowContextHolder tracks call-chain position through a ThreadLocal, which by definition only exists on the thread that set it. As soon as work is handed off to a different thread — a @Async method, a CompletableFuture running on a custom Executor, a task submitted to Executors.newVirtualThreadPerTaskExecutor() — that thread starts with no context of its own, and any @Logged call made from within it is reported as the start of a brand new chain instead of a continuation of the caller's chain. This class closes that gap for any Executor-based hand-off, including virtual thread executors: an Executor is an Executor regardless of the kind of thread backing it, so no separate handling is needed for virtual threads specifically.

Wrapping is a one-time setup step; call sites do not change:


 Executor virtualThreadExecutor =
         Executors.newVirtualThreadPerTaskExecutor();
 Executor propagating =
         new FlowContextPropagatingExecutor(virtualThreadExecutor);

 // Inside a @Logged method:
 propagating.execute(() -> {
     // Any @Logged call made here is recognized as part of the
     // caller's chain, at the same depth the caller was at.
     someOtherLoggedBean.doWork();
 });
 

The context is captured once per execute(Runnable) call, on the calling thread, at the moment the task is submitted — not when it starts running. This matters because a pooled Executor may not run the task immediately; capturing eagerly guarantees the propagated context reflects the caller's state at submission time, which is what a human reading the resulting call chain would expect, rather than whatever happened to be active on the calling thread later when the pool got around to it.

This class is deliberately not a Spring bean and requires no ApplicationContext: it only depends on FlowContextHolder and the Executor it wraps, so it can decorate any executor a consuming application already manages, including ones created outside of Spring's control.

  • Constructor Summary

    Constructors
    Constructor
    Description
    Wraps delegate so that every task submitted through this executor carries the submitting thread's FlowContext into whichever thread delegate actually runs it on.
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    execute(@NonNull Runnable command)
    Captures the calling thread's current FlowContext and MDC context map, and submits a wrapped task to the delegate executor that restores both before running command and restores the executor thread's own previous state again afterward, regardless of whether command completes normally or throws.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Constructor Details

    • FlowContextPropagatingExecutor

      public FlowContextPropagatingExecutor(Executor delegate)
      Wraps delegate so that every task submitted through this executor carries the submitting thread's FlowContext into whichever thread delegate actually runs it on.
      Parameters:
      delegate - the executor that will actually run submitted tasks
  • Method Details

    • execute

      public void execute(@NonNull Runnable command)
      Captures the calling thread's current FlowContext and MDC context map, and submits a wrapped task to the delegate executor that restores both before running command and restores the executor thread's own previous state again afterward, regardless of whether command completes normally or throws.

      The MDC context map is captured and restored in full, not limited to LoggedMdcKeys: see MdcContextPropagation for why.

      Specified by:
      execute in interface Executor
      Parameters:
      command - the task to run