1
0
Files
spring-security-demo/context-propagation/docs/02-async-virtual-threads.md
asmhatre 5e9e7f1b12 Split into per-article modules and add the method-security module
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
2026-08-25 02:01:29 +00:00

4.1 KiB

2. @Async, DelegatingSecurityContextExecutor, and virtual threads on Boot 4.1

← Prev: InheritableThreadLocal | Next: Structured concurrency →

Demo2AsyncVirtualThreads.java reproduces the exact executor bean Spring Boot 4.1 wires up when you set spring.threads.virtual.enabled=true: a SimpleAsyncTaskExecutor with setVirtualThreads(true). That bean backs @Async, MVC async request handling, and WebFlux's blocking-execution support. It is not a ThreadPoolTaskExecutor and never has a fixed pool of workers to reuse -- see Chapter 1 for why that matters.

Four scenarios, same question each time: does the async task see the Authentication that was active on the calling thread? Full output in docs/output/demo2.txt.

A) Default mode, unwrapped executor -- loses it

SecurityContextHolder's default strategy, MODE_THREADLOCAL, does not travel to any new thread, virtual or not. This is the exact symptom reported against Spring Security as gh-15040: swap in a raw virtual-thread executor and @Async methods start throwing AccessDeniedException because SecurityContextHolder.getContext().getAuthentication() is null.

B) MODE_INHERITABLETHREADLOCAL, unwrapped executor -- works

This is the finding from Chapter 1 applied to Spring Security directly. Because the virtual thread the executor spins up is fresh every time, MODE_INHERITABLETHREADLOCAL propagates the context correctly with zero extra wrapping code. The reference docs' warning against this mode predates virtual threads and is about pooled platform threads specifically -- it does not apply to this executor shape. This is still a global JVM-wide setting, so weigh that against the next two options, which are scoped to one executor bean.

C) DelegatingSecurityContextExecutor -- still works, unconditionally

DelegatingSecurityContextExecutor doesn't rely on thread-local inheritance at all -- it wraps the submitted Runnable, and the wrapper explicitly calls SecurityContextHolder.setContext(...) / clearContext() around the delegate's run(), wherever that run() happens to execute. That is why it has worked, unchanged, since long before virtual threads existed, and why it is still the correct choice for library code that cannot assume the application has set MODE_INHERITABLETHREADLOCAL globally.

D) ContextPropagatingTaskDecorator -- the mechanism that's actually new

This is the one that did not exist when the original version of this post went up. Spring Security 6.5 (GA 2025-05-19) added SecurityContextHolderThreadLocalAccessor, which self-registers with Micrometer's ContextRegistry via ServiceLoader the moment io.micrometer:context-propagation is on the classpath -- no bean, no configuration. Spring Framework's ContextPropagatingTaskDecorator (since 6.1) uses that registry to snapshot and restore every registered ThreadLocalAccessor around a task. Set it as the executor's task decorator and @Async methods get the SecurityContext back without any DelegatingSecurityContext* wrapper at all -- and the same decorator simultaneously restores MDC and tracing context, which the Delegating* classes never touched.

SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setVirtualThreads(true);
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());

Spring Security's own Concurrency Support page still only documents the Delegating* family as of 7.1.1 -- this pattern is real and shipped, just not yet reflected in that page.

io.micrometer:context-propagation is already on the classpath of any Boot 4.1 app that pulls in micrometer-observation (actuator, tracing, or spring-boot-starter-micrometer-*). If your app doesn't have a Micrometer dependency anywhere, add io.micrometer:context-propagation:1.2.1 (the version Boot 4.1.1's BOM manages) explicitly.