Spring Framework 6.2 added support for offloading non-reactive method to an executor, which Spring Boot configures when virtual threads are enabled.
I was hoping I could simply change my app that's currently running with Spring WebMVC and Tomcat to Spring WebFlux and Netty when enabling virtual threads.
There are several RestControllers that perform blocking database and rest calls, and they work as expected when I change the underlying web application type and server.
I am, however, missing a way to inject non-reactive ContextHolder variants that use thread locals, such as RequestContextHolder, LocaleContextHolder and SecurityContextHolder.
I am aware I could wrap each method with reactive variants, but I'd appreaciate if there was a way I could intercept the method call and map the mentioned ContextHolders from their Reactive counterparts without having to modify any code.
Anything that wraps method::invoke
call to target method into something that can access ServerWebExchange
would be welcome.
Comment From: krezovic
By adding io.micrometer:context-propagation
dependency and setting spring.reactor.context-propagation=auto
, I was able to access ReactiveSecurityContextHolder using ReactiveSecurityContextHolder.getContext().block()
in an Aspect.
I will investigate further how to make use of context propagation lib. You may close this issue if you don't think anything else needs to be done.
Comment From: krezovic
Further investigation showed that there are already Thread Local Accessors for remaining ContextHolders. I am providing an example in case this pops up in a search
import io.micrometer.context.ContextRegistry;
import org.springframework.context.i18n.LocaleContextThreadLocalAccessor;
import org.springframework.core.Ordered;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.util.function.Function;
@Component
public class ThreadLocalsFilter implements WebFilter, Ordered {
static {
var registry = ContextRegistry.getInstance();
registry.registerThreadLocalAccessor(new LocaleContextThreadLocalAccessor());
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.defaultIfEmpty(SecurityContextHolder.createEmptyContext())
.flatMap(ctx -> chain.filter(exchange).contextWrite(writeCtx(exchange, ctx)));
}
private Function<Context, Context> writeCtx(ServerWebExchange exchange, SecurityContext ctx) {
return context -> {
context =
context.put(LocaleContextThreadLocalAccessor.KEY, exchange.getLocaleContext());
if (ctx.getAuthentication() != null) {
return context.put(SecurityContext.class.getName(), ctx);
}
return context.delete(SecurityContext.class.getName());
};
}
}
Nothing to do from Spring side.