Moves the existing virtual-thread/context-propagation project into context-propagation/ and adds method-security/ for the Spring Security 7 method-security article: nine runnable demos, fourteen assertions, and every transcript the article quotes, regenerated by scripts/run-all.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
4.1 KiB
4. Executor, ExecutorService, and AsyncTaskExecutor wrapping
← Prev: Structured concurrency | Next: Reactive context →
Chapters 1–3 are all about virtual threads and structured concurrency, which came later.
Demo4ExecutorWrapping.java goes back to the baseline the post's "Using @Async", "Using
ExecutorService", and "Using CompletableFuture" sections describe: a fixed platform-thread
pool (ThreadPoolExecutor, ThreadPoolTaskExecutor), the shape almost every Spring app used
before spring.threads.virtual.enabled existed, and the exact pooled-thread danger
Chapter 1 demonstrated for InheritableThreadLocal. Full
output in docs/output/demo4.txt.
The three wrapper classes the post names
DelegatingSecurityContextExecutorServicewraps an entireExecutorService--execute(),submit(),invokeAll(),invokeAny()all go through the wrapper. This is the fix for the post'sTaskExecutionService("Using ExecutorService") example.DelegatingSecurityContextExecutorwraps a plainExecutorand is what you hand toCompletableFuture.supplyAsync(supplier, executor)-- the fix forCompletableFutureService("Using CompletableFuture"). The commonForkJoinPoolthatCompletableFuture.supplyAsync(supplier)uses when you don't supply an executor never propagates context; scenario C2 in the output shows that directly.DelegatingSecurityContextAsyncTaskExecutorwraps Spring's ownAsyncTaskExecutor/TaskExecutorabstraction -- the typeAsyncConfigurer#getAsyncExecutor()actually returns, and the object Spring's@Asyncinfrastructure callsexecute()/submit()on. This is the fix for the post'sAsyncConfigexample.
All three extend the same mechanism Chapter 2 already
described: wrap the submitted Runnable/Callable, capture SecurityContextHolder.getContext()
once (at wrap time, not at thread-construction time), and push/pop it around the delegate's
execution on whatever thread that turns out to be.
The edge case Chapter 1 sets up and this chapter resolves
Chapter 1's whole point was that a reused pool worker keeps whatever
InheritableThreadLocal value existed when the pool created it, not what the submitting
thread had at submission time -- that's why MODE_INHERITABLETHREADLOCAL is dangerous with a
fixed thread pool. The EDGE scenario in this demo asks the same question of the
Delegating* wrappers, and the answer is the opposite:
EDGE) task 1 on possibly-reused worker: authenticated as carol-task1
EDGE) task 2, same pool, different caller context: authenticated as dave-task2 <-- correct, NOT stale
Two tasks submitted through DelegatingSecurityContextExecutorService to the same two-worker
pool, with the caller's SecurityContextHolder context changed in between, each see their
own context -- never the other task's. That's because the wrapper captures context per
submission (inside wrap(), called synchronously from execute()/submit()), not once per
worker thread the way thread-local inheritance does. This is the actual reason the
Delegating* family predates virtual threads by years and is still correct on a fixed pool:
it never depended on thread identity in the first place.
Chapter 8 pins this exact contrast as a JUnit assertion
(delegatingSecurityContextExecutorServicePropagatesAndCapturesIndependentlyPerTask), not just
a printed line.
Practical note: which one do you actually need?
If you already have an AsyncConfigurer returning a ThreadPoolTaskExecutor, wrap it with
DelegatingSecurityContextAsyncTaskExecutor and nothing else changes -- @Async keeps working
as written. If you're calling CompletableFuture.supplyAsync(...) without an explicit executor
anywhere in the codebase, that's the common-ForkJoinPool trap in scenario C2; the fix is
always to supply a DelegatingSecurityContextExecutor-wrapped executor, never to reach for
MODE_INHERITABLETHREADLOCAL as a global patch for one call site.