Spring Web MVC的异步请求解读

文章目录

    • [一、 背景](#一、 背景)
    • [二、 核心概念:为什么要异步?](#二、 核心概念:为什么要异步?)
    • [三、 三种核心的异步处理方式](#三、 三种核心的异步处理方式)
      • [1. `Callable` (最简单的异步)](#1. Callable (最简单的异步))
      • [2. `DeferredResult` (解耦生产者与消费者)](#2. DeferredResult (解耦生产者与消费者))
      • [3. `ResponseBodyEmitter` / `SseEmitter` (流式响应)](#3. ResponseBodyEmitter / SseEmitter (流式响应))
      • [4. 响应式类型 (`Flux` / `Mono`)](#4. 响应式类型 (Flux / Mono))
    • [四、 深入原理:请求生命周期](#四、 深入原理:请求生命周期)
    • [五、 Spring Boot 3 的新特性:虚拟线程](#五、 Spring Boot 3 的新特性:虚拟线程)
    • [六、 配置与异常处理](#六、 配置与异常处理)
      • [1. 异步请求超时配置](#1. 异步请求超时配置)
      • [2. 异步拦截器](#2. 异步拦截器)
      • [3. 异步请求中的异常处理](#3. 异步请求中的异常处理)
    • [七、 总结与避坑指南](#七、 总结与避坑指南)
    • [八、 参考文档](#八、 参考文档)

一、 背景

在 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 来执行它。

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

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

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

java 复制代码
// 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。

java 复制代码
@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 上,异步处理有了新玩法。

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

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

java 复制代码
// 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 秒。

yaml 复制代码
spring:
  mvc:
    async:
      request-timeout: 30000 # 毫秒

2. 异步拦截器

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

java 复制代码
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 异步处理的核心密码。


八、 参考文档

相关推荐
AugustRed6 小时前
基于现有的 Controller 接口 API 暴露 MCP
spring·mcp
WZTTMoon6 小时前
Spring Boot 中Servlet、Filter、Listener 四种注册方式全解析
spring boot·后端·servlet
standovon7 小时前
Spring Boot整合Redisson的两种方式
java·spring boot·后端
MX_93597 小时前
SpringMVC请求参数
java·后端·spring·servlet·apache
zs宝来了7 小时前
Spring Boot 自动配置原理:@EnableAutoConfiguration 的魔法
spring boot·自动配置·源码解析·enableautoconfiguration
计算机学姐10 小时前
基于SpringBoot的咖啡店管理系统【个性化推荐+数据可视化统计+配送信息】
java·vue.js·spring boot·后端·mysql·信息可视化·tomcat
My的梦想已实现10 小时前
关于JAVA Springboot集成支付后打包JAR之后报安全错误的处理
java·spring boot·jar
小江的记录本11 小时前
【注解】常见 Java 注解系统性知识体系总结(附《全方位对比表》+ 思维导图)
java·前端·spring boot·后端·spring·mybatis·web
Mr.456711 小时前
Spring Boot 集成 PostgreSQL 表级备份与恢复实战
java·spring boot·后端·postgresql
白露与泡影11 小时前
探索springboot程序打包docker的最佳方式
spring boot·后端·docker