一、 背景

  在 Spring Boot 3 (基于 Spring Framework 6 和 Jakarta EE 10) 中,Spring MVC 对异步请求的支持已经达到了非常成熟且深度的整合水平。这不仅仅是简单的“多线程”,而是建立在 Servlet 3.1+ 规范之上的完整异步处理模型。

以下是对 Spring MVC 异步请求处理的全面讲解:


二、 核心概念:为什么要异步?

在传统的同步 Servlet 模型中:

  1. 请求到达,Tomcat 线程池分配一个线程。
  2. 线程执行业务逻辑(如查数据库、调用外部 API)。
  3. 线程在等待 IO 响应期间被阻塞
  4. 响应返回,线程释放。

痛点:高并发下,线程池会被大量等待中的请求耗尽,导致服务无法响应新请求。

Spring MVC 异步处理

  1. 请求到达,Tomcat 线程分配。
  2. 线程触发耗时操作(如返回 CallableDeferredResult),立即退出方法,释放回线程池。
  3. 耗时操作在独立线程或 IO 多路复用机制中执行。
  4. 执行完毕后,触发回调,重新请求一个 Tomcat 线程来处理响应。

优势:Tomcat 线程不再被 IO 阻塞,极大地提高了服务器的吞吐能力。


三、 三种核心的异步处理方式

Spring MVC 提供了三种主要的异步返回类型,分别对应不同的业务场景:

1. Callable (最简单的异步)

适用场景:需要在独立线程中执行的耗时任务,Spring 内部会使用配置好的 TaskExecutor 来执行它。

@GetMapping("/callable")
public Callable<String> processData() {
    // 这个 lambda 会由 Spring 提交到线程池执行
    return () -> {
        Thread.sleep(2000); // 模拟耗时
        return "Callable Result";
    };
}
  • 流程:主线程返回 Callable -> Spring 调用线程池执行 -> 等待结果 -> 分发结果给容器。
  • 注意:在 Spring Boot 3.2+ 支持虚拟线程的环境下,Callable 可以配置为在虚拟线程中执行,从而避免阻塞平台线程。

2. DeferredResult (解耦生产者与消费者)

适用场景:异步编程模型的核心。适用于需要在不同线程(如消息队列监听器、外部事件)中设置结果的场景。

// 1. 创建一个 DeferredResult 对象,设置超时时间
DeferredResult<String> result = new DeferredResult<>(5000L);

// 2. 模拟在另一个线程(如 MQ 消费者、定时任务)中设置结果
new Thread(() -> {
    try { Thread.sleep(2000); } catch (Exception e) {}
    // 当这里调用 setResult 时,Servlet 容器会重新唤醒请求处理
    result.setResult("Hello World");
}).start();

// 3. Controller 方法立即返回,此时还没有结果
return result;
  • 优势:完全解耦。请求线程和处理线程没有任何直接关联,非常适合网关转发、长轮询等场景。

3. ResponseBodyEmitter / SseEmitter (流式响应)

适用场景:需要分批次、多次发送数据给客户端(如 AI 对话、实时日志推送)。SseEmitterResponseBodyEmitter 的子类,专门用于 Server-Sent Events。

@GetMapping("/stream")
public SseEmitter streamData() {
    SseEmitter emitter = new SseEmitter();
    // 在其他线程中向 emitter 发送数据
    executorService.execute(() -> {
        try {
            emitter.send("First chunk");
            Thread.sleep(1000);
            emitter.send("Second chunk");
            emitter.complete(); // 结束流
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });
    return emitter;
}

4. 响应式类型 (Flux / Mono)

这是 Spring Boot 3 混合架构的亮点。如果你的 Controller 返回 FluxMono,Spring MVC 会自动将其适配为异步处理。

  • 返回 Mono:类似于 DeferredResultCallable
  • 返回 Flux:自动转换为 ResponseBodyEmitter 流式处理。

四、 深入原理:请求生命周期

当异步请求发生时,Servlet 容器会经历以下阶段:

  1. 初始分发 (REQUEST)

    • Tomcat 接收请求。
    • Spring MVC DispatcherServlet 处理请求。
    • Controller 返回异步类型(如 Flux)。
    • 关键动作:调用 request.startAsync(),开启异步模式。
    • 主线程结束,返回 Tomcat 线程池。
  2. 异步阶段

    • 业务逻辑在后台执行(Reactive 线程池或自定义线程池)。
    • 此阶段 不占用 Tomcat 线程
  3. 异步分发 (ASYNC)

    • 当后台任务完成(或 Flux 发射数据时),Servlet 容器会发起一个新的分发
    • 这个分发的类型是 DispatcherType.ASYNC
    • DispatcherServlet 再次介入,负责将结果写回响应流。

重点:过滤器 和拦截器 的行为差异。

  • 过滤器:默认只在 REQUEST 阶段执行。如果想拦截 ASYNC 阶段,需在 web.xmlFilterRegistrationBean 中显式配置 DispatcherType.ASYNC
  • 拦截器默认会拦截所有阶段(包括 ASYNC)。这就是为什么你的 SaInterceptor 会在流式数据写回时被再次触发的原因。

五、 Spring Boot 3 的新特性:虚拟线程

在 Spring Boot 3.2+ 中,如果运行在 JDK 21 上,异步处理有了新玩法。

传统上,异步编程是为了解决线程阻塞问题,但代码写起来复杂(回调地狱)。

虚拟线程 让你可以重新写同步阻塞的代码,但获得异步非阻塞的性能:

// Spring Boot 3.2 + JDK 21
// 开启虚拟线程后,这个简单的阻塞代码不会阻塞 Tomcat 的平台线程
@GetMapping("/blocking-with-virtual")
public String blocking() throws InterruptedException {
    Thread.sleep(1000); // 阻塞的是虚拟线程,底层 OS 线程不阻塞
    return "Done";
}

这改变了异步请求的格局:

  • 简单业务:使用虚拟线程 + 同步代码,不再需要 FluxCallable
  • 流式业务:依然需要 FluxSseEmitter,因为这是流式数据模型,不仅仅是线程模型。

六、 配置与异常处理

1. 异步请求超时配置

异步请求不能无限等待,默认超时通常是 30 秒。

spring:
  mvc:
    async:
      request-timeout: 30000 # 毫秒

2. 异步拦截器

为了处理异步生命周期中的事件(如超时、完成),Spring 提供了 AsyncHandlerInterceptor

public class MyAsyncInterceptor implements AsyncHandlerInterceptor {
    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 在异步处理开始之后调用(主线程退出前)
        // 可以在这里清理 ThreadLocal,或者记录日志
        System.out.println("Async handling started, main thread released.");
    }
}

3. 异步请求中的异常处理

异步阶段抛出的异常,会被 Spring MVC 捕获,并通过 ASYNC 分发转发给异常处理器。

  • 如果返回 DeferredResult,可以通过 result.setErrorResult(e) 处理。
  • 如果返回 Flux,异常会导致流终止,并被全局异常处理器捕获(前提是异常处理器能处理 SSE 类型的响应,否则会报 Converter 错误)。

七、 总结与避坑指南

在 Spring Boot 3 中使用 Spring MVC 异步请求:

  1. 首选响应式类型:如果涉及到流式输出(SSE),Flux 是最优雅的方案。
  2. 警惕 ThreadLocal:任何基于 ThreadLocal 的组件(Sa-Token, Spring Security, MDC 日志追踪)在异步分发阶段都会失效。
    • 解决方案:要么使用 AsyncHandlerInterceptor 手动传播上下文;要么在主线程入口处提取数据,显式传参(推荐)。
  3. 拦截器配置:必须显式排除 DispatcherType.ASYNC,除非你明确需要在数据写回时做拦截。
  4. 过滤器配置:如果你需要在 ASYNC 阶段也执行某些过滤器(如重新绑定上下文),记得配置 dispatcherTypes 包含 ASYNC

理解了 “REQUEST -> startAsync -> ASYNC Dispatch” 这一流程,你就掌握了 Spring MVC 异步处理的核心密码。


八、 参考文档

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐