每个类似
@RequestMapping
注释的方法都会出现该问题,该方法接受一个用 @RequestBody
注释的参数。在这种情况下,该方法不会在 WebFlux 的 ExecutorScheduler
/TaskExecutor
托管线程 (task-<N>
) 上执行,而是在 WebFlux 事件循环 (reactor-http-nio-<N>
) 上执行。
因此,这个问题并不特定于虚拟线程,显然它出现在平台线程中,如果平台
TaskExecutor
,例如ThreadPoolTaskExecutor
,配置为运行控制器的方法。相反,这个问题特定于 Spring-WebFlux 桥与 WebFlux 本身的交互,可能是 Mono
子类的复杂层次结构(GET 请求落在 Just
上,带有主体的请求 - 位于其他东西上)。这个桥,WebFluxConfigurationSupport
,将所谓的阻塞执行器设置为RequestMappingHandlerAdapter
,负责调用控制器的方法。
事实上,这样的配置是 Spring Web-Flux 集成的最新介绍。 Rossen Stoyanchev 在 2023 年 6 月 15 日的文章阻止 WebFlux 控制器方法的执行中写道:
我们可以通过阻止执行的选项来增强 WebFuxConfigurer,例如配置 AsyncTaskExecutor,例如新的 VirtualThreadTaskExecutor。
因此,如果我们使用 Spring Boot 2.5.0 版本(它带来了 WebFlux 3.4.6),那么我们可以看到,即使是 GET 请求也不是在执行器上执行,而是在 WebFlux 事件循环上执行。因此,这种思想对于 Spring WebFlux 来说相对较新,我们可能期望对其进行进一步改进,同时将这种情况报告为错误。
与此同时,作为临时解决方法,我可以提供简单且批发的(дубовoe)解决方案,该解决方案仍然允许在执行程序管理的线程上运行有问题的方法。
AOP 方面拦截对这些方法的所有调用,并在其管理的线程上调用它们。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class ManagedThreadAspect {
private static class Worker implements Runnable {
private final ProceedingJoinPoint pjp;
private volatile Object result;
private volatile Throwable exception;
public Worker(final ProceedingJoinPoint pjp) {
this.pjp = pjp;
}
@Override
public void run() {
try {
result = pjp.proceed();
} catch (Throwable e) {
exception = e;
}
}
public Object getResult() throws Throwable {
if (exception != null)
throw exception;
return result;
}
}
@Around("execution(* *(.., @org.springframework.web.bind.annotation.RequestBody (*), ..))")
public Object invokeOnManagedThread(final ProceedingJoinPoint pjp) throws Throwable {
final Worker worker = new Worker(pjp); // this is nothing more than a result holder
Thread.ofVirtual().start( () -> {
worker.run();
}).join();
return worker.getResult();
}
}
或者,如果您想重用库存执行器,并因此使用
spring.threads.virtual.enabled=true
设置,则注入执行器并调用它:
@Autowired
private AsyncTaskExecutor taskExecutor;
...
final Worker worker = new Worker(pjp);
taskExecutor.submit(worker).get();
return worker.getResult();
效果与 WebFlux 管理的执行器调用此方法大致相同:
@RequestBody
方法将在虚拟(或者通常是 Aspect 管理)线程上调用。
Spring AOP应该适当配置,例如其中一个
@Configuration
类应使用 @EnableAspectJAutoProxy(proxyTargetClass=true)
进行注释。
如果上述方法对你有用,我可以在 github 上发布一个示例,或者向你做一个 PR。
然而,从概念上讲,在 WebFlux 中使用虚拟线程会引发一些问题。 WebFlux 的关键思想是在其事件循环中“非阻塞”处理请求,而虚拟线程本质上带来的是“阻塞”。因此,WebFlux 和虚拟线程即使不是相互排斥的,也被许多人视为并发方法。例如,Baldun 博客的作者,其标题为“Reactor WebFlux vs Virtual Threads”,得出的结论是: 我们比较了两种不同的并发和异步处理方法。
当然,这并不意味着虚拟线程在 WebFlux 中应该被视为无稽之谈,只有运行现实世界的 Web 应用程序才能揭示这种架构的优缺点。