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
3.2 KiB
3. StructuredTaskScope and SecurityContext
← Prev: Async + virtual threads | Next: Executor/ExecutorService wrapping →
Demo3StructuredConcurrency.java asks the Chapter 2 question again, but for
StructuredTaskScope (JEP 505, fifth preview in JDK 25 -- still preview through the JDK 26
sixth preview per JEP 525, so every example here needs --enable-preview). A fork() call
starts a brand new virtual thread for the subtask, same as the executors in Chapter 2, so the
Chapter 1 finding applies here too. Full output in
docs/output/demo3.txt.
What the JEP actually promises
JEP 525's text is explicit about one kind of context and silent about another:
Subtasks forked in a scope inherit
ScopedValuebindings.
That is a real, specified guarantee -- and it says nothing about ThreadLocal. Spring
Security's SecurityContextHolder is a ThreadLocal/InheritableThreadLocal, not a
ScopedValue. Nothing in the structured concurrency API changes that, and scenario A below
proves it: a plain scope.fork(...) with the default MODE_THREADLOCAL strategy loses the
Authentication exactly like the unwrapped executor in Chapter 2 did.
Four scenarios
-
A) Plain fork, MODE_THREADLOCAL -- lost. The default
SecurityContextHolderstrategy isn't inherited by anything, structured concurrency included. -
B) Plain fork, MODE_INHERITABLETHREADLOCAL -- propagates. Same reasoning as Chapter 2, scenario B:
fork()'s subtask thread is a fresh virtual thread, so inheritance at construction time works and there is no pooled-thread staleness risk. -
C) Manual capture-and-restore around the forked
Callable-- propagates, and does not depend on the global strategy mode at all:SecurityContext captured = SecurityContextHolder.getContext(); Callable<String> task = () -> { SecurityContextHolder.setContext(captured); try { return doWork(); } finally { SecurityContextHolder.clearContext(); } }; scope.fork(task);This is the safest pattern for a
StructuredTaskScopeused inside library code, the same wayDelegatingSecurityContextExecutoris the safest pattern for anExecutor: it works regardless of what the surrounding application has setSecurityContextHolder's strategy to. -
D)
ContextSnapshot.wrap(...)around the forkedCallable-- the Chapter 2 mechanism applied tofork()instead ofexecute(). BecauseSecurityContextHolderThreadLocalAccessoris already registered with Micrometer'sContextRegistry,ContextSnapshotFactory.builder() .build().captureAll()picks up the currentSecurityContext(and MDC, and tracing context) in one call, and.wrap(callable)restores all of them inside the subtask. This is the version worth reaching for once you have more than theSecurityContextto carry across the scope boundary.
The practical takeaway
StructuredTaskScope does not give SecurityContextHolder anything for free. If your
fork()ed subtasks need to call secured services, wrap them explicitly -- option C if you
want zero new dependencies, option D if context-propagation is already on the classpath and
you have other thread-locals to carry along too.